断言工具的编写_编写干净的测试–用特定领域的语言替换断言
斷言工具的編寫
很難為干凈的代碼找到一個好的定義,因為我們每個人都有自己的單詞clean的定義。 但是,有一個似乎是通用的定義:
簡潔的代碼易于閱讀。
這可能會讓您感到有些驚訝,但我認為該定義也適用于測試代碼。 使測試盡可能具有可讀性是我們的最大利益,因為:
- 如果我們的測試易于閱讀,那么很容易理解我們的代碼是如何工作的。
- 如果我們的測試易于閱讀,那么如果測試失敗(不使用調試器),很容易發現問題。
編寫干凈的測試并不難,但是需要大量的實踐,這就是為什么如此多的開發人員為此苦苦掙扎的原因。
我也為此感到掙扎,這就是為什么我決定與您分享我的發現的原因。
這是本教程的第五部分,介紹了如何編寫干凈的測試。 這次,我們將使用特定于域的語言替換斷言。
數據不是那么重要
在我以前的博客文章中,我確定了以數據為中心的測試引起的兩個問題。 盡管該博客文章討論了新對象的創建,但是這些問題對于斷言也有效。
讓我們刷新內存,看一下單元測試的源代碼,該代碼可確保當使用唯一電子郵件地址和社交符號創建新用戶帳戶時, RepositoryUserService類的registerNewUserAccount(RegistrationForm userAccountData)方法能夠按預期工作在提供者中。
我們的單元測試如下所示(相關代碼突出顯示):
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);} }如我們所見,從單元測試中找到的斷言可確保返回的User對象的屬性值正確。 我們的主張確保:
- email屬性的值正確。
- firstName屬性的值正確。
- lastName屬性的值正確。
- signInProvider的值正確。
- 角色屬性的值正確。
- 密碼為空。
這當然很明顯,但是以這種方式重復這些斷言很重要,因為它可以幫助我們確定斷言的問題。 我們的斷言是以數據為中心的 ,這意味著:
- 讀者必須知道返回對象的不同狀態 。 例如,如果我們考慮示例,讀者必須知道,如果返回的RegistrationForm對象的email , firstName , lastName和signInProvider屬性具有非null值,并且password屬性的值為null,則意味著對象是通過使用社交登錄提供程序進行的注冊。
- 如果創建的對象具有許多屬性,則我們的斷言會亂碼我們測試的源代碼。 我們應該記住,即使我們要確保返回對象的數據正確無誤,但描述返回對象的狀態也更為重要。
讓我們看看如何改善斷言。
將斷言轉變為特定領域的語言
您可能已經注意到,開發人員和領域專家通常在相同的事情上使用不同的術語。 換句話說,開發人員講的語言與領域專家講的語言不同。 這在開發人員和領域專家之間造成了不必要的混亂和摩擦 。
域驅動設計(DDD)為該問題提供了一種解決方案。 埃里克·埃文斯(Eric Evans)在他的《 域驅動設計 》( Domain-Driven Design)一書中引入了泛在語言一詞。
維基百科指定了普遍使用的語言 ,如下所示:
無處不在的語言是圍繞領域模型構造的語言,所有團隊成員都使用該語言將團隊的所有活動與軟件聯系起來。
如果我們想寫斷言使用“正確的”語言,那么我們必須彌合開發人員和領域專家之間的鴻溝。 換句話說,我們必須創建一種特定于域的語言來編寫斷言。
實施我們的領域特定語言
在實現我們特定領域的語言之前,我們必須對其進行設計。 當我們為斷言設計特定領域的語言時,我們必須遵循以下規則:
我不會在這里進行詳細說明,因為這是一個巨大的主題,不可能在單個博客中進行解釋。 如果要了解有關特定于域的語言和Java的更多信息,可以通過閱讀以下博客文章開始:
- Java Fluent API設計器速成課程
- 用Java創建DSL,第1部分:什么是領域特定語言?
- 用Java創建DSL,第2部分:流利性和上下文
- 用Java創建DSL,第3部分:內部和外部DSL
- 用Java創建DSL,第4部分:元編程很重要
如果遵循這兩個規則,則可以為特定于域的語言創建以下規則:
- 用戶具有名字,姓氏和電子郵件地址。
- 用戶是注冊用戶。
- 用戶是使用社交符號提供者注冊的,這意味著該用戶沒有密碼。
現在,我們已經指定了特定領域語言的規則,我們已經準備好實現它。 我們將通過創建一個自定義的AssertJ斷言來實現此目的,該斷言實現我們特定于域的語言的規則。
我不會在此博客文章中描述所需的步驟,因為我已經寫了一篇博客來描述這些步驟 。 如果您不熟悉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;} }現在,我們已經創建了一種特定于域的語言,用于將斷言寫入User對象。 下一步是修改單元測試,以使用我們新的領域特定語言。
用特定于域的語言替換JUnit斷言
在重寫斷言以使用特定于域的語言之后,單元測試的源代碼如下所示(相關部分已突出顯示):
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);} }我們的解決方案具有以下優點:
- 我們的斷言使用領域專家可以理解的語言。 這意味著我們的測試是可執行的規范,它易于理解并且始終是最新的。
- 我們不必浪費時間弄清楚測試失敗的原因。 我們的自定義錯誤消息可確保我們知道失敗的原因。
- 如果User類的API發生了變化,我們不必修復所有將斷言寫入User對象的測試方法。 我們唯一需要更改的類是UserAssert類。 換句話說,將實際的斷言邏輯從測試方法中移開會使我們的測試不那么脆弱,更易于維護。
讓我們花點時間總結一下我們從此博客文章中學到的知識。
摘要
現在,我們已將斷言轉換為特定領域的語言。 這篇博客文章教會了我們三件事:
- 遵循以數據為中心的方法會在開發人員和領域專家之間造成不必要的混亂和摩擦。
- 為我們的斷言創建一種特定于域的語言會使我們的測試不那么困難,因為實際的斷言邏輯已移至自定義斷言類。
- 如果我們使用特定領域的語言編寫斷言,則會將測試轉換為可執行的規范,這些規范易于理解和說出領域專家的語言。
翻譯自: https://www.javacodegeeks.com/2014/06/writing-clean-tests-replace-assertions-with-a-domain-specific-language.html
斷言工具的編寫
總結
以上是生活随笔為你收集整理的断言工具的编写_编写干净的测试–用特定领域的语言替换断言的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 会计事务所从事证券业务备案流程(会计事务
- 下一篇: 查找文件内容 linux(查找linux