编写干净的测试–天堂中的麻烦
如果我們的代碼有明顯的錯(cuò)誤,我們很有動(dòng)力進(jìn)行改進(jìn)。 但是,在某些時(shí)候,我們認(rèn)為我們的代碼“足夠好”并繼續(xù)前進(jìn)。
通常,當(dāng)我們認(rèn)為改進(jìn)現(xiàn)有代碼的好處小于所需的工作時(shí),就會(huì)發(fā)生這種情況。 當(dāng)然,如果我們低估了投資的回報(bào),我們可能會(huì)打錯(cuò)電話,這會(huì)傷害我們。
這就是發(fā)生在我身上的事情,我決定寫這篇文章,以便您避免犯同樣的錯(cuò)誤。
編寫“良好”單元測(cè)試
如果我們要編寫“好的”單元測(cè)試,則必須編寫以下單元測(cè)試:
- 只測(cè)試一件事 。 好的單元測(cè)試只能因一個(gè)原因而失敗,并且只能斷言一件事。
- 被正確命名 。 測(cè)試方法的名稱必須顯示測(cè)試失敗的原因。
- 模擬外部依賴關(guān)系(和狀態(tài)) 。 如果單元測(cè)試失敗,我們將確切知道問題出在哪里。
補(bǔ)充閱讀:
- 單元測(cè)試只能測(cè)試一件事情
- 編寫干凈的測(cè)試:命名問題
- 編寫干凈的測(cè)試:分而治之
- 編寫干凈的測(cè)試:驗(yàn)證或不驗(yàn)證
如果我們編寫滿足這些條件的單元測(cè)試,我們將編寫好的單元測(cè)試。 對(duì)?
我曾經(jīng)這樣認(rèn)為。 現(xiàn)在我對(duì)此表示懷疑 。
善意鋪平地獄之路
我從未見過決定編寫糟糕的單元測(cè)試的軟件開發(fā)人員。 如果開發(fā)人員正在編寫單元測(cè)試,則他/她很有可能要編寫好的單元測(cè)試。 但是,這并不意味著該開發(fā)人員編寫的單元測(cè)試是好的。
我想編寫既易于閱讀又易于維護(hù)的單元測(cè)試。 我什至寫了一個(gè)教程,描述了如何編寫干凈的測(cè)試 。 問題在于,本教程中給出的建議還不夠好(尚未)。 它可以幫助我們?nèi)腴T,但是并沒有顯示出兔子洞的真正深度。
我的教程中描述的方法存在兩個(gè)主要問題:
命名標(biāo)準(zhǔn)是FTW嗎?
如果我們使用Roy Osherove引入的“命名標(biāo)準(zhǔn)”,則會(huì)注意到很難描述被測(cè)狀態(tài)和預(yù)期行為。
當(dāng)我們?yōu)楹唵螆鼍熬帉憸y(cè)試時(shí),此命名標(biāo)準(zhǔn)非常有效。 問題在于,真正的軟件并不簡單。 通常,我們最終使用以下兩個(gè)選項(xiàng)之一來命名測(cè)試方法:
首先 ,如果我們嘗試盡可能具體,則測(cè)試方法的方法名稱會(huì)變得太過糟糕。 最后,我們必須承認(rèn)我們不能像我們想要的那樣具體,因?yàn)榉椒Q會(huì)占用太多空間。
其次 ,如果我們嘗試使方法名稱盡可能短,則方法名稱將不會(huì)真正描述測(cè)試狀態(tài)和預(yù)期行為。
選擇哪個(gè)選項(xiàng)實(shí)際上并不重要,因?yàn)闊o論如何我們都會(huì)遇到以下問題:
- 如果測(cè)試失敗,則方法名稱不一定描述要出錯(cuò)的方法。 我們可以使用自定義斷言來解決此問題,但是它們不是免費(fèi)的。
- 很難對(duì)我們的測(cè)試涵蓋的場景進(jìn)行簡要概述。
以下是我們?cè)凇?編寫干凈測(cè)試”教程中編寫的測(cè)試方法的名稱:
- registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException()
- registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount()
- registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider()
- registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount()
- registerNewUserAccount_SocialSignInAnquequeEmail_ShouldNotCreateEncodedPasswordForUser()
這些方法的名稱不是很長,但是我們必須記住,編寫這些單元測(cè)試是為了測(cè)試一種簡單的注冊(cè)方法。 當(dāng)我使用這種命名約定為現(xiàn)實(shí)生活中的軟件項(xiàng)目編寫自動(dòng)化測(cè)試時(shí),最長的方法名稱是我們最長的示例名稱的兩倍。
那不是很干凈或可讀。 我們可以做得更好 。
沒有通用配置
在本教程中,我們使單元測(cè)試變得更好了 。 盡管如此,他們?nèi)匀辉馐苓@樣的事實(shí),即沒有“自然的”方式在不同的單元測(cè)試之間共享配置。
這意味著我們的單元測(cè)試包含許多重復(fù)的代碼,這些代碼配置了我們的模擬對(duì)象并創(chuàng)建了在單元測(cè)試中使用的其他對(duì)象。
另外,由于沒有“自然”的方式表明某些常量僅與特定的測(cè)試方法相關(guān),因此我們必須將所有常量添加到測(cè)試類的開頭。
我們的測(cè)試類的源代碼如下(突出顯示有問題的代碼):
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 com.googlecode.catchexception.CatchException.catchException; import static com.googlecode.catchexception.CatchException.caughtException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.never; 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_SocialSignInAndDuplicateEmail_ShouldThrowException() 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(new User());catchException(registrationService).registerNewUserAccount(registration);assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);}@Testpublic void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() 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(new User());catchException(registrationService).registerNewUserAccount(registration);verify(repository, never()).save(isA(User.class));}@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();assertThatUser(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);assertThatUser(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);} }一些開發(fā)人員認(rèn)為看起來像上面示例的單元測(cè)試足夠干凈。 我理解這種情緒,因?yàn)槲以?jīng)是其中之一。 但是,這些單元測(cè)試存在三個(gè)問題:
換句話說,這些單元測(cè)試很難閱讀,很難編寫和維護(hù)。 我們必須做得更好 。
摘要
這篇博客文章教會(huì)了我們四件事:
- 即使我們認(rèn)為我們正在編寫好的單元測(cè)試,也不一定是正確的。
- 如果由于必須更改許多單元測(cè)試而導(dǎo)致更改現(xiàn)有功能的速度很慢,那么我們就不會(huì)編寫好的單元測(cè)試。
- 如果添加新功能的速度很慢,因?yàn)槲覀儽仨毾騿卧獪y(cè)試中添加大量重復(fù)的代碼,那么我們就不會(huì)編寫好的單元測(cè)試。
- 如果我們看不到單元測(cè)試所涵蓋的情況,那么我們就沒有編寫好的單元測(cè)試。
本教程的下一部分將回答這個(gè)非常相關(guān)的問題:
如果現(xiàn)有的單元測(cè)試很爛,我們?cè)撊绾谓鉀Q?
如果要編寫干凈的測(cè)試,則應(yīng)閱讀我的“ 編寫干凈的測(cè)試”教程 。
翻譯自: https://www.javacodegeeks.com/2015/03/writing-clean-tests-trouble-in-paradise.html
總結(jié)
以上是生活随笔為你收集整理的编写干净的测试–天堂中的麻烦的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: nfc卡快捷键(华为nfc卡快捷键)
- 下一篇: 使用tinylog 1.0简化您的日志记