编写干净的测试–分而治之
好的單元測試應(yīng)該僅出于一個原因而失敗。 這意味著適當?shù)膯卧獪y試僅測試一個邏輯概念。
如果我們要編寫干凈的測試,則必須識別這些邏輯概念,并且每個邏輯概念僅編寫一個測試用例。
這篇博客文章描述了我們?nèi)绾巫R別從測試中發(fā)現(xiàn)的邏輯概念,以及如何將現(xiàn)有的單元測試分成多個單元測試。
干凈還不夠好
讓我們先看一下單元測試的源代碼,該源代碼確保當使用唯一的電子郵件地址和社交登錄提供者創(chuàng)建新用戶帳戶時, RepositoryUserService類的registerNewUserAccount(RegistrationForm userAccountData)方法能夠按預(yù)期工作。
該單元測試的源代碼如下所示:
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 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);} }這個單元測試非常干凈。 畢竟,我們的測試類,測試方法以及在測試方法內(nèi)部創(chuàng)建的局部變量具有描述性名稱。 我們還用常數(shù)替換了幻數(shù),并創(chuàng)建了特定領(lǐng)域的語言來創(chuàng)建新對象和編寫斷言。
但是, 我們可以使這項測試更好 。
這個單元測試的問題是它可能由于多種原因而失敗。 如果發(fā)生以下情況,它將失敗:
換句話說,此單元測試測試了四個不同的邏輯概念,這導致以下問題:
- 如果此測試失敗,我們不一定知道為什么失敗。 這意味著我們必須閱讀單元測試的源代碼。
- 單元測試有點長,這使得閱讀起來有些困難。
- 很難描述預(yù)期的行為。 這意味著很難為我們的測試方法找到好名字。
通過確定該單元測試將失敗的情況,我們可以確定單個單元測試所涵蓋的邏輯概念。
這就是為什么我們需要將此測試分為四個單元測試。
一測試,一故障
下一步是將單元測試分成四個新的單元測試,并確保每個單元測試都測試一個邏輯概念。 我們可以通過編寫以下單元測試來做到這一點:
編寫完這些單元測試之后,測試類的源代碼如下所示:
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; 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 net.petrikainulainen.spring.social.signinmvc.user.model.UserAssert.assertThat; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; 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 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_ShouldCheckThatEmailIsUnique() 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);registrationService.registerNewUserAccount(registration);verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider() 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);registrationService.registerNewUserAccount(registration);ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);verify(repository, times(1)).save(userAccountArgument.capture());User createdUserAccount = userAccountArgument.getValue();assertThat(createdUserAccount).hasEmail(REGISTRATION_EMAIL_ADDRESS).hasFirstName(REGISTRATION_FIRST_NAME).hasLastName(REGISTRATION_LAST_NAME).isRegisteredUser().isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount() 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);}@Testpublic void registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser() 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);registrationService.registerNewUserAccount(registration);verifyZeroInteractions(passwordEncoder);} }編寫僅測試一個邏輯概念的單元測試的明顯好處是,很容易知道為什么測試失敗。 但是,此方法還有其他兩個好處:
- 指定期望的行為很容易。 這意味著更容易為我們的測試方法找出好名字。
- 由于這些單元測試比原始單元測試要短得多,因此更容易弄清測試方法/組件的要求。 這有助于我們將測試轉(zhuǎn)換為可執(zhí)行規(guī)范。
讓我們繼續(xù)并總結(jié)從這篇博客文章中學到的知識。
摘要
現(xiàn)在,我們已經(jīng)成功地將單元測試分為四個較小的單元測試,它們測試了一個邏輯概念。 這篇博客文章教會了我們兩件事:
- 我們了解到,通過確定測試失敗的情況,我們可以確定單個單元測試所涵蓋的邏輯概念。
- 我們了解到,編寫僅測試一個邏輯概念的單元測試有助于我們將測試用例編寫成可執(zhí)行的規(guī)范,從而確定測試方法/組件的要求。
翻譯自: https://www.javacodegeeks.com/2014/06/writing-clean-tests-divide-and-conquer.html
總結(jié)
以上是生活随笔為你收集整理的编写干净的测试–分而治之的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 623058开头是什么银行?
- 下一篇: 招银汇金黄金交易有杠杆吗?