七大罪过与如何避免
在整個(gè)本文中,我將在代碼片段中使用Java,同時(shí)還將使用JUnit和Mockito 。
本文旨在提供示例測試代碼,這些示例可以是:
- 難以閱讀
- 難以維護(hù)
在這些示例之后,本文將嘗試提供替代方法,這些替代方法可用于增強(qiáng)測試的可讀性,從而有助于使其在將來更易于維護(hù)。
創(chuàng)建良好的示例具有挑戰(zhàn)性,因此,作為讀者,我鼓勵(lì)您將示例僅用作了解本文基本信息的工具,以力求實(shí)現(xiàn)可讀的測試代碼。
1.通用測試名稱
您可能已經(jīng)看到了如下命名的測試
@Test void testTranslator() {String word = new Translator().wordFrom(1);assertThat(word, is("one")); }現(xiàn)在這是非常通用的,不會(huì)通知代碼的讀者測試實(shí)際上正在測試什么。 Translator可能有多種方法,我們?nèi)绾沃罍y試中正在使用哪種方法? 通過查看測試名稱并不清楚,這意味著我們必須查看測試本身才能看到。
我們可以做得更好,因此可以看到以下內(nèi)容:
@Test void translate_from_number_to_word() {String word = new Translator().wordFrom(1);assertThat(word, is("one")); }從上面我們可以看到,它在解釋該測試實(shí)際上在做什么方面做得更好。 此外,如果你的名字你的測試文件類似TranslatorShould你可以當(dāng)你把測試文件和單個(gè)測試名稱形成在你心目中是合理的一句話: Translator should translate from number to word 。
2.測試設(shè)置中的變異
在測試中很有可能會(huì)希望將測試中使用的對象構(gòu)造為處于特定狀態(tài)。 有不同的方法,下面顯示了一種這樣的方法。 在此代碼段中,我們基于該對象中包含的信息來確定某個(gè)字符是否實(shí)際上是“ Luke Skywalker”(想象這就是isLuke()方法的作用):
@Test void inform_when_character_is_luke_skywalker() {StarWarsTrivia trivia = new StarWarsTrivia();Character luke = new Character();luke.setName("Luke Skywalker");Character vader = new Character();vader.setName("Darth Vader");luke.setFather(vader);luke.setProfession(PROFESSION.JEDI);boolean isLuke = trivia.isLuke(luke);assertTrue(isLuke); }上面構(gòu)造了一個(gè)Character對象來表示“ Luke Skywalker”,此后發(fā)生的事涉及相當(dāng)比例的突變。 它繼續(xù)在隨后的行中設(shè)置名稱,父母身份和職業(yè)。 當(dāng)然,這忽略了與我們的朋友“達(dá)斯·維達(dá)”發(fā)生的類似事情。
這種突變水平分散了測試中正在發(fā)生的事情。 如果我們再回顧一下我先前的句子:
在測試中很有可能您希望將測試中使用的對象構(gòu)造為處于特定狀態(tài)
但是,上述測試實(shí)際上發(fā)生了兩個(gè)階段:
- 構(gòu)造對象
- 使其處于某種狀態(tài)
這是不必要的,我們可以避免。 可能有人建議,為了避免發(fā)生突變,我們可以簡單地將所有內(nèi)容都移植并轉(zhuǎn)儲(chǔ)到構(gòu)造函數(shù)中,以確保我們以給定狀態(tài)構(gòu)造對象,從而避免發(fā)生突變:
@Test void inform_when_character_is_luke_skywalker() {StarWarsTrivia trivia = new StarWarsTrivia();Character vader = new Character("Darth Vader");Character luke = new Character("Luke Skywalker", vader, PROFESSION.JEDI);boolean isLuke = trivia.isLuke(luke);assertTrue(isLuke); }從上面我們可以看到,我們減少了代碼行的數(shù)量以及對象的變異。 但是,在此過程中,我們已經(jīng)失去了Character (現(xiàn)在為Character參數(shù))在測試中表示的含義。 為了使isLuke()方法返回true,我們傳入的Character對象必須具有以下內(nèi)容:
- “盧克·天行者”的名字
- 有一個(gè)父親叫“達(dá)斯·維達(dá)”
- 成為絕地武士
但是,從這種情況的測試中還不清楚,我們必須檢查Character的內(nèi)部以了解這些參數(shù)的用途(或者您的IDE會(huì)告訴您)。
我們可以做的更好,我們可以利用Builder模式在所需狀態(tài)下構(gòu)造一個(gè)Character對象,同時(shí)還可以保持測試的可讀性:
@Test void inform_when_character_is_luke_skywalker() {StarWarsTrivia trivia = new StarWarsTrivia();Character luke = CharacterBuilder().aCharacter().withNameOf("Luke Skywalker").sonOf(new Character("Darth Vader")).employedAsA(PROFESSION.JEDI).build();boolean isLuke = trivia.isLuke(luke);assertTrue(isLuke); }通過上面的內(nèi)容,可能還會(huì)有幾行內(nèi)容,但是它試圖解釋測試中的重要內(nèi)容。
3.斷言瘋狂
在測試期間,您將斷言/驗(yàn)證系統(tǒng)中是否發(fā)生了某些事情(通常位于每次測試結(jié)束時(shí))。 這是測試中非常重要的一步,可能很想添加許多斷言,例如斷言返回的對象的值。
@Test void successfully_upgrades_user() {UserService service = new UserService();User someBasicUser = UserBuilder.aUser().withName("Basic Bob").withAge(23).withTypeOf(UserType.BASIC).build();User upgradedUser = service.upgrade(someBasicUser);assertThat(upgradedUser.name(), is("Basic Bob"));assertThat(upgradedUser.type(), is(UserType.SUPER_USER));assertThat(upgradedUser.age(), is(23)); }(在上面的示例中,我向構(gòu)建器提供了其他信息,例如名稱和年齡,但是如果對測試不重要,通常不會(huì)包含此信息,請?jiān)跇?gòu)建器中使用明智的默認(rèn)值)
如我們所見,存在三個(gè)斷言,在更極端的示例中,我們談?wù)摰氖菙?shù)十行斷言。 我們不一定需要執(zhí)行三個(gè)斷言,有時(shí)我們可以合而為一:
@Test void successfully_upgrades_user() {UserService service = new UserService();User someBasicUser = UserBuilder.aUser().withName("Basic Bob").withAge(23).withTypeOf(UserType.BASIC).build();User expectedUserAfterUpgrading = UserBuilder.aUser().withName("Basic Bob").withAge(23).withTypeOf(UserType.SUPER_USER).build();User upgradedUser = service.upgrade(someBasicUser);assertThat(upgradedUser, is(expectedUserAfterUpgrading)); }現(xiàn)在,我們將升級后的用戶與我們希望對象在升級后的外觀進(jìn)行比較。 為此,您將需要比較的對象( User )具有覆蓋的equals和hashCode 。
4.神奇的價(jià)值觀
您是否曾經(jīng)看過數(shù)字或字符串并想知道它代表什么? 我已經(jīng)過了,那些不得不解析代碼行的寶貴時(shí)間可以很快加起來。 我們在下面有這樣的代碼示例。
@Test void denies_entry_for_someone_who_is_not_old_enough() {Person youngPerson = PersonBuilder.aPerson().withAgeOf(17).build();NightclubService service = new NightclubService(21);String decision = service.entryDecisionFor(youngPerson);assertThat(decision, is("No entry. They are not old enough.")); }閱讀以上內(nèi)容,您可能會(huì)遇到一些問題,例如:
- 17是什么意思?
- 21在構(gòu)造函數(shù)中是什么意思?
如果我們可以向代碼的讀者表示它們的含義,那不是很好,那么他們不必考慮太多嗎? 幸運(yùn)的是,我們可以:
private static final int SEVENTEEN_YEARS = 17; private static final int MINIMUM_AGE_FOR_ENTRY = 21; private static final String NO_ENTRY_MESSAGE = "No entry. They are not old enough.";@Test void denies_entry_for_someone_who_is_not_old_enough() {Person youngPerson = PersonBuilder.aPerson().withAgeOf(SEVENTEEN_YEARS).build();NightclubService service = new NightclubService(MINIMUM_AGE_FOR_ENTRY);String decision = service.entryDecisionFor(youngPerson);assertThat(decision, is(NO_ENTRY_MESSAGE)); }現(xiàn)在,當(dāng)我們看以上內(nèi)容時(shí),我們知道:
- SEVENTEEN_YEARS是用來表示17年的值,毫無疑問,我們已經(jīng)在讀者的腦海中留下了疑問。 不是秒或分鐘,而是年。
- MINIMUM_AGE_FOR_ENTRY是必須允許某人進(jìn)入夜總會(huì)的值。 讀者甚至不必關(guān)心該值是什么,而只是了解測試背景下的含義。
- NO_ENTRY_MESSAGE是返回的值,表示不允許某人進(jìn)入夜總會(huì)。 從本質(zhì)上講,字符串通常具有更好的描述性,但是請始終檢查代碼以找出可以改進(jìn)的地方。
這里的關(guān)鍵是減少代碼閱讀器嘗試解析代碼行所花費(fèi)的時(shí)間。
5.難以閱讀的測試名稱
@Test void testingNumberOneAndNumberTwoCanBeAddedTogetherToProduceNumberThree() {... }您花了多長時(shí)間閱讀以上內(nèi)容? 它易于閱讀嗎?您能快速了解一下此處正在測試的內(nèi)容嗎?還是需要解析許多字符?
幸運(yùn)的是,我們可以嘗試以更好的方式命名測試,方法是將測試減少到實(shí)際測試的水平,并刪除試圖添加的華夫餅:
@Test void twoNumbersCanBeAdded() {... }它的閱讀效果更好嗎? 我們減少了這里的單詞數(shù)量,更易于解析。 如果我們可以更進(jìn)一步,問我們是否可以放棄使用駱駝箱怎么辦:
@Test void two_numbers_can_be_added() {... }這是一個(gè)優(yōu)先事項(xiàng),應(yīng)該由對給定代碼庫做出貢獻(xiàn)的人員同意。 使用蛇形小寫字母(如上所述)可以幫助提高測試名稱的可讀性,因?yàn)槟芸赡艽蛩隳7聲婢渥印?因此,蛇形格的使用緊隨普通書面句子中存在的物理空間。 但是,Java不允許在方法名稱中使用空格,這是我們所擁有的最好的方法,缺少使用Spock之類的東西。
6.依賴注入的設(shè)置器
通常,對于測試,您希望能夠?yàn)榻o定對象(也稱為“協(xié)作對象”或簡稱為“協(xié)作者”)注入依賴關(guān)系。 為了達(dá)到這個(gè)目的,您可能已經(jīng)看到了類似以下內(nèi)容的內(nèi)容:
@Test void save_a_product() {ProductService service = new ProductService();TestableProductRepository repository = mock(TestableProductRepository.class);service.setRepository(repository);Product newProduct = new Product("some product");service.addProduct(newProduct);verify(repository).save(newProduct); }上面使用了setter方法,即setRepository() ,以便注入TestableProductRepository的模擬,因此我們可以驗(yàn)證服務(wù)和存儲(chǔ)庫之間是否發(fā)生了正確的協(xié)作。
類似于圍繞突變的觀點(diǎn),這里我們對ProductService進(jìn)行突變,而不是將對象構(gòu)造為所需的狀態(tài)。 可以通過將協(xié)作者注入構(gòu)造函數(shù)中來避免這種情況:
@Test void save_a_product() {TestableProductRepository repository = mock(TestableProductRepository.class);ProductService service = new ProductService(repository);Product newProduct = new Product("some product");service.addProduct(newProduct);verify(repository).save(newProduct); }因此,現(xiàn)在我們將協(xié)作者注入了構(gòu)造函數(shù)中,現(xiàn)在我們在構(gòu)造時(shí)就知道對象將處于什么狀態(tài)。但是,您可能會(huì)問“在此過程中我們是否沒有丟失某些上下文?”。
我們已經(jīng)從
service.setRepository(repository);至
ProductService service = new ProductService(repository);前者更具描述性。 因此,如果您不喜歡這種上下文丟失的情況,則可以選擇類似構(gòu)建器的內(nèi)容,并創(chuàng)建以下內(nèi)容:
@Test void save_a_product() {TestableProductRepository repository = mock(TestableProductRepository.class);ProductService service = ProductServiceBuilder.aProductService().withRepository(repository).build();Product newProduct = new Product("some product");service.addProduct(newProduct);verify(repository).save(newProduct); }該解決方案使我們能夠避免在通過withRepository()方法記錄協(xié)作者注入的情況下改變ProductService 。
7.非描述性驗(yàn)證
如前所述,您的測試通常會(huì)包含驗(yàn)證語句。 不用自己動(dòng)手,您通常會(huì)利用庫來執(zhí)行此操作。 但是,您必須注意不要掩蓋驗(yàn)證的意圖。 要了解我在說什么,請看以下示例。
@Test void no_error_is_shown_when_user_is_valid() {UIComponent component = mock(UIComponent.class);User user = mock(User.class);when(user.isValid()).thenReturn(true);LoginController controller = new LoginController();controller.attemptLogin(component, user);verifyZeroInteractions(component); }現(xiàn)在,如果您看上面的內(nèi)容,您是否立即知道該斷言表明沒有錯(cuò)誤顯示給用戶? 可能是因?yàn)樗菧y試的名稱,但是您可能不將該代碼行與測試名稱相關(guān)聯(lián) 。 這是因?yàn)樗荕ockito的代碼,并且通用以適應(yīng)許多不同的用例。 它按照它說的做,檢查與UIComponent的模擬是否沒有交互。
但是,這意味著您的測試有所不同。 我們?nèi)绾闻κ蛊涓忧逦?
@Test void no_error_is_shown_when_user_is_valid() {UIComponent component = mock(UIComponent.class);User user = mock(User.class);when(user.isValid()).thenReturn(true);LoginController controller = new LoginController();controller.attemptLogin(component, user);verify(component, times(0)).addErrorMessage("Invalid user"); }這樣會(huì)更好一些,因?yàn)榇舜a的讀者有很大的潛力可以快速了解此行的工作。 但是,在某些情況下,可能仍然很難閱讀。 在這種情況下,請按照以下說明提取一種方法,以更好地解釋您的驗(yàn)證。
@Test void no_error_is_shown_when_user_is_valid() {UIComponent component = mock(UIComponent.class);User user = mock(User.class);when(user.isValid()).thenReturn(true);LoginController controller = new LoginController();controller.attemptLogin(component, user);verifyNoErrorMessageIsAddedTo(component); }private void verifyNoErrorMessageIsAddedTo(UIComponent component) {verify(component, times(0)).addErrorMessage("Invalid user"); }上面的代碼并不完美,但是在當(dāng)前測試的范圍內(nèi),它肯定可以提供我們正在驗(yàn)證的內(nèi)容的高層次概述。
結(jié)束語
我希望您喜歡這篇文章,下次您完成編寫測試時(shí)將花費(fèi)一到兩個(gè)重構(gòu)步驟。 在下一次之前,我給你以下報(bào)價(jià):
“必須編寫程序供人們閱讀,并且只能偶然地使機(jī)器執(zhí)行。” ― Harold Abelson,計(jì)算機(jī)程序的結(jié)構(gòu)和解釋
翻譯自: https://www.javacodegeeks.com/2019/08/seven-testing-sins-and-how-to-avoid-them.html
總結(jié)
- 上一篇: Windows 11提高电池寿命的6个技
- 下一篇: asyncexec_如何安全使用SWT的