编写干净的测试–用特定领域的语言替换断言
很難為干凈的代碼找到一個(gè)好的定義,因?yàn)槲覀兠總€(gè)人都有自己的單詞clean的定義。 但是,有一個(gè)似乎是通用的定義:
干凈的代碼易于閱讀。
這可能會(huì)讓您感到有些驚訝,但是我認(rèn)為該定義也適用于測(cè)試代碼。 使測(cè)試盡可能具有可讀性是我們的最大利益,因?yàn)?#xff1a;
- 如果我們的測(cè)試易于閱讀,那么很容易理解我們的代碼是如何工作的。
- 如果我們的測(cè)試易于閱讀,那么如果測(cè)試失敗(不使用調(diào)試器),很容易發(fā)現(xiàn)問(wèn)題。
編寫干凈的測(cè)試并不難,但是需要大量的實(shí)踐,這就是為什么如此多的開(kāi)發(fā)人員為此苦苦掙扎的原因。
我也為此感到掙扎,這就是為什么我決定與您分享我的發(fā)現(xiàn)的原因。
這是本教程的第五部分,介紹了如何編寫干凈的測(cè)試。 這次,我們將使用特定于域的語(yǔ)言替換斷言。
數(shù)據(jù)不是那么重要
在我以前的博客文章中,我確定了以數(shù)據(jù)為中心的測(cè)試引起的兩個(gè)問(wèn)題。 盡管該博客文章討論了新對(duì)象的創(chuàng)建,但是這些問(wèn)題對(duì)于斷言也同樣有效。
讓我們刷新內(nèi)存,看一下單元測(cè)試的源代碼,該代碼可確保當(dāng)使用唯一的電子郵件地址和社交符號(hào)創(chuàng)建新的用戶帳戶時(shí), RepositoryUserService類的registerNewUserAccount(RegistrationForm userAccountData)方法能夠按預(yù)期工作在提供者中。
我們的單元測(cè)試如下(相關(guān)代碼突出顯示):
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class) public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);} }如我們所見(jiàn),從單元測(cè)試中找到的斷言可確保返回的User對(duì)象的屬性值正確。 我們的主張確保:
- email屬性的值正確。
- firstName屬性的值正確。
- lastName屬性的值正確。
- signInProvider的值正確。
- 角色屬性的值正確。
- 密碼為空。
這當(dāng)然很明顯,但是以這種方式重復(fù)這些斷言很重要,因?yàn)樗梢詭椭覀兇_定斷言的問(wèn)題。 我們的斷言是以數(shù)據(jù)為中心的 ,這意味著:
- 讀者必須知道返回對(duì)象的不同狀態(tài) 。 例如,如果我們考慮示例,讀者必須知道,如果返回的RegistrationForm對(duì)象的email , firstName , lastName和signInProvider屬性具有非null值,并且password屬性的值為null,則意味著對(duì)象是通過(guò)使用社交登錄提供程序進(jìn)行的注冊(cè)。
- 如果創(chuàng)建的對(duì)象具有許多屬性,則我們的斷言會(huì)亂碼我們測(cè)試的源代碼。 我們應(yīng)該記住,即使我們要確保返回的對(duì)象的數(shù)據(jù)正確無(wú)誤,但描述返回對(duì)象的狀態(tài)更為重要。
讓我們看看如何改善斷言。
將斷言變成特定領(lǐng)域的語(yǔ)言
您可能已經(jīng)注意到,開(kāi)發(fā)人員和領(lǐng)域?qū)<彝ǔT谙嗤氖虑樯鲜褂貌煌男g(shù)語(yǔ)。 換句話說(shuō),開(kāi)發(fā)人員講的語(yǔ)言與領(lǐng)域?qū)<抑v的語(yǔ)言不同。 這在開(kāi)發(fā)人員和領(lǐng)域?qū)<抑g造成了不必要的混亂和摩擦 。
域驅(qū)動(dòng)設(shè)計(jì)(DDD)為該問(wèn)題提供了一種解決方案。 埃里克·埃文斯(Eric Evans)在他的名為《 域驅(qū)動(dòng)設(shè)計(jì) 》( Domain-Driven Design)的書中引入了泛在語(yǔ)言一詞。
維基百科指定了普遍使用的語(yǔ)言 ,如下所示:
無(wú)處不在的語(yǔ)言是圍繞領(lǐng)域模型構(gòu)造的語(yǔ)言,所有團(tuán)隊(duì)成員都使用該語(yǔ)言將團(tuán)隊(duì)的所有活動(dòng)與軟件聯(lián)系起來(lái)。
如果我們想寫斷言使用“正確的”語(yǔ)言,則必須彌合開(kāi)發(fā)人員和領(lǐng)域?qū)<抑g的鴻溝。 換句話說(shuō),我們必須創(chuàng)建一種特定于域的語(yǔ)言來(lái)編寫斷言。
實(shí)施我們的領(lǐng)域特定語(yǔ)言
在實(shí)現(xiàn)我們特定領(lǐng)域的語(yǔ)言之前,我們必須對(duì)其進(jìn)行設(shè)計(jì)。 當(dāng)為斷言設(shè)計(jì)特定領(lǐng)域的語(yǔ)言時(shí),我們必須遵循以下規(guī)則:
我不會(huì)在這里進(jìn)行詳細(xì)說(shuō)明,因?yàn)檫@是一個(gè)巨大的主題,不可能在單個(gè)博客中進(jìn)行解釋。 如果您想了解有關(guān)領(lǐng)域特定語(yǔ)言和Java的更多信息,可以通過(guò)閱讀以下博客文章開(kāi)始:
- Java Fluent API設(shè)計(jì)器速成課程
- 用Java創(chuàng)建DSL,第1部分:什么是領(lǐng)域特定語(yǔ)言?
- 用Java創(chuàng)建DSL,第2部分:流利性和上下文
- 用Java創(chuàng)建DSL,第3部分:內(nèi)部和外部DSL
- 用Java創(chuàng)建DSL,第4部分:元編程很重要
如果遵循這兩個(gè)規(guī)則,則可以為特定于域的語(yǔ)言創(chuàng)建以下規(guī)則:
- 用戶具有名字,姓氏和電子郵件地址。
- 用戶是注冊(cè)用戶。
- 用戶是使用社交符號(hào)提供者注冊(cè)的,這意味著該用戶沒(méi)有密碼。
現(xiàn)在,我們已經(jīng)指定了特定領(lǐng)域語(yǔ)言的規(guī)則,我們已經(jīng)準(zhǔn)備好實(shí)施它。 我們將通過(guò)創(chuàng)建一個(gè)自定義的AssertJ斷言來(lái)實(shí)現(xiàn)此目的,該斷言實(shí)現(xiàn)我們特定于域的語(yǔ)言的規(guī)則。
我不會(huì)在此博客文章中描述所需的步驟,因?yàn)槲乙呀?jīng)寫了一篇博客來(lái)描述這些步驟 。 如果您不熟悉AssertJ,建議您先閱讀該博客文章,然后再閱讀本博客文章的其余部分。
我們的自定義斷言類的源代碼如下所示:
mport org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assertions;public class UserAssert extends AbstractAssert<UserAssert, User> {private UserAssert(User actual) {super(actual, UserAssert.class);}public static UserAssert assertThat(User actual) {return new UserAssert(actual);}public UserAssert hasEmail(String email) {isNotNull();Assertions.assertThat(actual.getEmail()).overridingErrorMessage( "Expected email to be <%s> but was <%s>",email,actual.getEmail()).isEqualTo(email);return this;}public UserAssert hasFirstName(String firstName) {isNotNull();Assertions.assertThat(actual.getFirstName()).overridingErrorMessage("Expected first name to be <%s> but was <%s>",firstName,actual.getFirstName()).isEqualTo(firstName);return this;}public UserAssert hasLastName(String lastName) {isNotNull();Assertions.assertThat(actual.getLastName()).overridingErrorMessage( "Expected last name to be <%s> but was <%s>",lastName,actual.getLastName()).isEqualTo(lastName);return this;}public UserAssert isRegisteredByUsingSignInProvider(SocialMediaService signInProvider) {isNotNull();Assertions.assertThat(actual.getSignInProvider()).overridingErrorMessage( "Expected signInProvider to be <%s> but was <%s>",signInProvider,actual.getSignInProvider()).isEqualTo(signInProvider);hasNoPassword();return this;}private void hasNoPassword() {isNotNull();Assertions.assertThat(actual.getPassword()).overridingErrorMessage("Expected password to be <null> but was <%s>",actual.getPassword()).isNull();}public UserAssert isRegisteredUser() {isNotNull();Assertions.assertThat(actual.getRole()).overridingErrorMessage( "Expected role to be <ROLE_USER> but was <%s>",actual.getRole()).isEqualTo(Role.ROLE_USER);return this;} }現(xiàn)在,我們已經(jīng)創(chuàng)建了一種特定于域的語(yǔ)言,用于將斷言寫入U(xiǎn)ser對(duì)象。 下一步是修改單元測(cè)試,以使用我們新的領(lǐng)域特定語(yǔ)言。
用特定于域的語(yǔ)言替換JUnit斷言
在重寫斷言以使用特定于域的語(yǔ)言之后,單元測(cè)試的源代碼如下所示(相關(guān)部分已突出顯示):
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.security.crypto.password.PasswordEncoder;import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class) public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertThat(createdUserAccount).hasEmail(REGISTRATION_EMAIL_ADDRESS).hasFirstName(REGISTRATION_FIRST_NAME).hasLastName(REGISTRATION_LAST_NAME).isRegisteredUser().isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);} }我們的解決方案具有以下優(yōu)點(diǎn):
- 我們的斷言使用領(lǐng)域?qū)<铱梢岳斫獾恼Z(yǔ)言。 這意味著我們的測(cè)試是一個(gè)可執(zhí)行的規(guī)范,它易于理解并且始終是最新的。
- 我們不必浪費(fèi)時(shí)間弄清楚測(cè)試失敗的原因。 我們的自定義錯(cuò)誤消息可確保我們知道失敗的原因。
- 如果User類的API發(fā)生了變化,我們不必修復(fù)所有將斷言寫入U(xiǎn)ser對(duì)象的測(cè)試方法。 我們唯一需要更改的類是UserAssert類。 換句話說(shuō),將實(shí)際的斷言邏輯從測(cè)試方法中移開(kāi)會(huì)使我們的測(cè)試不那么脆弱,更易于維護(hù)。
讓我們花點(diǎn)時(shí)間總結(jié)一下我們從此博客文章中學(xué)到的知識(shí)。
摘要
現(xiàn)在,我們已將斷言轉(zhuǎn)換為特定領(lǐng)域的語(yǔ)言。 這篇博客文章教會(huì)了我們?nèi)?#xff1a;
- 遵循以數(shù)據(jù)為中心的方法會(huì)在開(kāi)發(fā)人員和領(lǐng)域?qū)<抑g造成不必要的混亂和摩擦。
- 為我們的斷言創(chuàng)建一種特定于域的語(yǔ)言會(huì)使我們的測(cè)試不那么困難,因?yàn)閷?shí)際的斷言邏輯已移至自定義斷言類。
- 如果我們使用特定領(lǐng)域的語(yǔ)言編寫斷言,則會(huì)將測(cè)試轉(zhuǎn)換為可執(zhí)行的規(guī)范,這些規(guī)范易于理解并且會(huì)說(shuō)領(lǐng)域?qū)<业恼Z(yǔ)言。
翻譯自: https://www.javacodegeeks.com/2014/06/writing-clean-tests-replace-assertions-with-a-domain-specific-language.html
總結(jié)
以上是生活随笔為你收集整理的编写干净的测试–用特定领域的语言替换断言的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 部落冲突华为版电脑(部落冲突华为最新版)
- 下一篇: JPA 2.1实体图–第2部分:在运行时