怎样编写测试类测试分支_编写干净的测试-被认为有害的新内容
怎樣編寫測試類測試分支
很難為干凈的代碼找到一個好的定義,因為我們每個人都有自己的單詞clean的定義。 但是,有一個似乎是通用的定義:
簡潔的代碼易于閱讀。
這可能會讓您感到有些驚訝,但我認為該定義也適用于測試代碼。 使測試盡可能具有可讀性是我們的最大利益,因為:
- 如果我們的測試易于閱讀,那么很容易理解我們的代碼是如何工作的。
- 如果我們的測試易于閱讀,那么如果測試失敗(不使用調試器),很容易發現問題。
編寫干凈的測試并不難,但是需要大量的實踐,這就是為什么如此多的開發人員為此苦苦掙扎的原因。
我也為此感到掙扎,這就是為什么我決定與您分享我的發現的原因。
這是我教程的第四部分,描述了我們如何編寫干凈的測試。 這次我們將學習為什么不使用new關鍵字在測試方法中創建對象。 我們還將學習如何用工廠方法和測試數據構建器替換new關鍵字。
新不是新黑
在本教程中,我們一直在重構單元測試,以確保當使用唯一的電子郵件地址和社交登錄提供者創建新用戶帳戶時, RepositoryUserService類的registerNewUserAccount(RegistrationForm userAccountData)方法能夠按預期工作。
RegistrationForm類是一個數據傳輸對象(DTO) ,我們的單元測試使用setter方法設置其屬性值。 我們的單元測試的源代碼如下所示(相關代碼突出顯示):
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 RegistrationForm();registration.setEmail(REGISTRATION_EMAIL_ADDRESS);registration.setFirstName(REGISTRATION_FIRST_NAME);registration.setLastName(REGISTRATION_LAST_NAME);registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);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);} }那么,有什么問題呢? 我們的單元測試中突出顯示的部分很短,而且相對容易閱讀。 我認為,此代碼的最大問題是它是以數據為中心的。 它創建了一個新的RegistrationForm對象并設置了創建對象的屬性值,但沒有描述這些屬性值的含義。
如果我們使用new關鍵字在測試方法中創建新對象,則由于以下原因,我們的測試將變得難以閱讀:
盡管不能完全消除這些缺點是不現實的,但我們應盡最大努力將其影響降到最低,并使我們的測試盡可能易于閱讀。
讓我們找出如何使用工廠方法來做到這一點。
使用工廠方法
當我們使用工廠方法創建新對象時,我們應該以這種方式命名工廠方法及其方法參數,以使我們的代碼更易于讀寫。 讓我們看一下兩種不同的工廠方法,看看它們對我們的單元測試的可讀性有什么樣的影響。
這些工廠方法通常添加到對象母類中,因為它們通常對多個測試類有用。 但是,由于我想保持簡單,因此將它們直接添加到測試類中。
第一個工廠方法的名稱是newRegistrationViaSocialSignIn() ,并且沒有方法參數。 在將此工廠方法添加到測試類之后,單元測試的源如下所示(相關部分已突出顯示):
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 = newRegistrationViaSocialSignIn();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);}private RegistrationForm newRegistrationViaSocialSignIn() {RegistrationForm registration = new RegistrationForm();registration.setEmail(REGISTRATION_EMAIL_ADDRESS);registration.setFirstName(REGISTRATION_FIRST_NAME);registration.setLastName(REGISTRATION_LAST_NAME);registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);return registration;} }第一種工廠方法具有以下后果:
- 我們測試方法的一部分,它創建了新的RegistrationForm對象,比以前干凈得多,并且工廠方法的名稱描述了所創建的RegistrationForm對象的狀態。
- 我們的模擬對象的配置更難以閱讀,因為email屬性的值在我們的工廠方法中被“隱藏”了。
- 由于創建的RegistrationForm對象的屬性值被“隱藏”在我們的工廠方法中,因此我們的斷言更難以閱讀。
如果使用對象母模式 ,則問題將更大,因為我們必須將相關的常量移至對象母類。
我認為可以說,盡管第一種工廠方法有其好處,但它也有嚴重的缺點。
讓我們看看第二種工廠方法是否可以消除這些缺點。
第二個工廠方法的名稱為newRegistrationViaSocialSignIn() ,并且它將電子郵件地址,名字,姓氏和提供程序中的社交符號作為方法參數。 在將此工廠方法添加到測試類之后,單元測試的源如下所示(相關部分已突出顯示):
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 = newRegistrationViaSocialSignIn(REGISTRATION_EMAIL_ADDRESS,REGISTRATION_FIRST_NAME,REGISTRATION_LAST_NAME,SOCIAL_MEDIA_SERVICE);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);}private RegistrationForm newRegistrationViaSocialSignIn(String emailAddress, String firstName, String lastName, SocialMediaService signInProvider) {RegistrationForm registration = new RegistrationForm();registration.setEmail(emailAddress);registration.setFirstName(firstName);registration.setLastName(lastName);registration.setSignInProvider(signInProvider);return registration;} }第二種工廠方法具有以下后果:
- 我們的測試方法的一部分(創建新的RegistrationForm對象)比使用第一個工廠方法的相同代碼稍微有些混亂。 但是,它仍然比原始代碼干凈,因為factory方法的名稱描述了創建對象的狀態。
- 似乎消除了第一個工廠方法的弊端,因為創建的對象的屬性值未“隱藏”在工廠方法內部。
看起來很酷,對吧?
真的很容易想到天堂里一切都很好,但是事實并非如此。 盡管我們已經看到工廠方法可以使我們的測試更具可讀性,但事實是,只有在滿足以下條件時,它們才是一個不錯的選擇:
讓我們找出測試數據生成器是否可以解決其中一些問題。
使用測試數據構建器
測試數據構建器是使用構建器模式創建新對象的類。 Effective Java中描述的構建器模式有很多好處 ,但是我們的主要動機是提供一種流暢的API以創建測試中使用的對象。
我們可以按照以下步驟創建一個測試數據構建器類,該類創建新的RegistrationForm對象:
我們的測試數據構建器類的源代碼如下所示:
public class RegistrationFormBuilder {private RegistrationForm registration;public RegistrationFormBuilder() {registration = new RegistrationForm();}public RegistrationFormBuilder email(String email) {registration.setEmail(email);return this;}public RegistrationFormBuilder firstName(String firstName) {registration.setFirstName(firstName);return this;}public RegistrationFormBuilder lastName(String lastName) {registration.setLastName(lastName);return this;}public RegistrationFormBuilder isSocialSignInViaSignInProvider(SocialMediaService signInProvider) {registration.setSignInProvider(signInProvider);return this;}public RegistrationForm build() {return registration;} }在修改了單元測試以使用新的測試數據構建器類之后,其源代碼如下所示(相關部分已突出顯示):
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);} }如我們所見,測試數據構建器具有以下優點:
- 創建新的RegistrationForm對象的代碼易于閱讀和編寫。 我非常喜歡流暢的API,并且我認為這段代碼既優美又優雅。
- 構建器模式可確保從我們的測試數據中發現的變化不再是問題,因為我們可以簡單地將新方法添加到測試數據構建器類中。
- 模擬對象和斷言的配置易于閱讀,因為常量在我們的測試方法中可見,并且DSL強調每個屬性值的含義。
那么,我們應該對所有內容使用構建器模式嗎?
沒有!
僅在有意義時,才應使用測試數據構建器。 換句話說,我們應該在以下情況下使用它們:
如果滿足以下條件之一,則構建器模式是一個完美的選擇。 原因是我們可以通過命名builder類的setter-like方法來創建特定于域的語言 。 即使我們將創建許多不同的對象并設置許多屬性值,這也使我們的測試易于讀寫。
那是建造者木匠的力量。
如果您想了解有關流利API的更多信息,則應閱讀以下文章:
- 流利的界面
- Java Fluent API設計器速成課程
- 用Java構建流暢的API(內部DSL)
今天就這些。 讓我們繼續并總結從這篇博客文章中學到的知識。
摘要
我們了解了為什么使用new關鍵字在測試方法中創建對象不是一個好主意,并且我們學習了兩種不同的方法來創建在測試中使用的對象。
更具體地說,這篇博客文章教會了我們三件事:
- 通過使用new關鍵字在測試方法中創建所需的對象是一個壞主意,因為它會使我們的測試混亂且難以閱讀。
- 如果我們只需要設置少數幾個屬性值,而我們的測試數據沒有太多變化,則應該使用工廠方法來創建所需的對象。
- 如果必須設置很多屬性值和/或我們的測試數據有很多差異,則應該使用測試數據生成器來創建所需的對象。
翻譯自: https://www.javacodegeeks.com/2014/05/writing-clean-tests-new-considered-harmful.html
怎樣編寫測試類測試分支
總結
以上是生活随笔為你收集整理的怎样编写测试类测试分支_编写干净的测试-被认为有害的新内容的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 冲量定理是什么 冲量定理是什么意思
- 下一篇: 定律是什么意思 定律有什么意思