开发罪过_七大罪过与如何避免
開發罪過
在整個本文中,我將在代碼片段中使用Java,同時還將使用JUnit和Mockito 。
本文旨在提供以下測試代碼示例:
- 難以閱讀
- 難以維護
在這些示例之后,本文將嘗試提供替代方法,這些替代方法可用于增強測試的可讀性,從而有助于使其在將來更易于維護。
創建良好的示例具有挑戰性,因此,作為讀者,我鼓勵您將示例僅用作了解本文基本信息的工具,以力求實現可讀的測試代碼。
1.通用測試名稱
您可能已經看到了如下所示的測試
@Test void testTranslator() {String word = new Translator().wordFrom(1);assertThat(word, is("one")); }現在這是非常通用的,不會通知代碼的讀者該測試實際在測試什么。 Translator可能有多種方法,我們如何知道測試中正在使用哪種方法? 通過查看測試名稱并不清楚,這意味著我們必須查看測試本身才能看到。
我們可以做得更好,因此可以看到以下內容:
@Test void translate_from_number_to_word() {String word = new Translator().wordFrom(1);assertThat(word, is("one")); }從上面的內容可以看出,它在解釋此測試的實際作用方面做得更好。 此外,如果您將測試文件命名為TranslatorShould那么在將測試文件和單個測試名稱組合在一起時,您應該在頭腦中形成一個合理的句子: Translator should translate from number to word 。
2.測試設置中的變異
在測試中,您很有可能希望將測試中使用的對象構造為處于特定狀態。 有不同的方法,下面顯示了一種這樣的方法。 在此代碼段中,我們基于該對象中包含的信息來確定某個字符是否實際上是“ 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); }上面的代碼構造了一個Character對象來表示“ Luke Skywalker”,此后發生的事涉及相當比例的突變。 它繼續在隨后的行中設置名稱,父母身份和職業。 當然,這忽略了與我們的朋友“達斯·維達”發生的類似事情。
這種突變水平分散了測試中正在發生的事情。 如果我們再回顧一下我先前的句子:
在測試中很有可能您希望將測試中使用的對象構造為處于特定狀態
但是,上述測試實際上發生了兩個階段:
- 構造對象
- 使其處于某種狀態
這是不必要的,我們可以避免。 可能有人建議,為了避免發生突變,我們可以簡單地將所有內容都移植并轉儲到構造函數中,以確保我們以給定的狀態構造對象,避免發生突變:
@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); }從上面可以看到,我們減少了代碼行的數量以及對象的變異。 但是,在此過程中,我們已經失去了Character (現在為Character參數)在測試中表示的含義。 為了使isLuke()方法返回true,我們傳入的Character對象必須具有以下內容:
- “盧克·天行者”的名字
- 有一個父親叫“達斯·維達”
- 成為絕地武士
但是,從這種情況的測試中尚不清楚,我們必須檢查Character的內部以了解這些參數的用途(否則您的IDE會告訴您)。
我們可以做得更好,可以利用Builder模式在所需狀態下構造一個Character對象,同時還可以保持測試的可讀性:
@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); }通過上面的內容,可能還會有幾行內容,但是它試圖解釋測試中的重要內容。
3.斷言瘋狂
在測試期間,您將斷言/驗證系統中是否發生了某些事情(通常位于每次測試結束時)。 這是測試中非常重要的一步,可能很想添加許多斷言,例如斷言返回對象的值。
@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)); }(在上面的示例中,我向構建器提供了其他信息,例如名稱和年齡,但是,如果對測試不重要,則通常不會包含此信息,請在構建器中使用明智的默認值)
如我們所見,存在三個斷言,在更極端的示例中,我們談論的是數十行斷言。 我們不一定需要執行三個斷言,有時我們可以合而為一:
@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)); }現在,我們將升級后的用戶與我們期望對象在升級后的外觀進行比較。 為此,您將需要比較的對象( User )具有覆蓋的equals和hashCode 。
4.神奇的價值觀
您是否曾經看過數字或字符串并想知道它代表什么? 我已經擁有了那些不得不解析代碼行的寶貴時間,這些時間很快就會開始累加起來。 我們在下面有一個這樣的代碼示例。
@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.")); }閱讀以上內容,您可能會遇到一些問題,例如:
- 17是什么意思?
- 21在構造函數中是什么意思?
如果我們可以向代碼讀者表示它們的含義,那不是很好,那么他們不必考慮太多嗎? 幸運的是,我們可以:
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)); }現在,當我們看以上內容時,我們知道:
- SEVENTEEN_YEARS是用來表示17年的值,毫無疑問,我們已經在讀者的腦海中留下了疑問。 不是秒或分鐘,而是年。
- MINIMUM_AGE_FOR_ENTRY是必須允許某人進入夜總會的值。 讀者甚至不必關心此值是什么,只需了解測試上下文中的含義即可。
- NO_ENTRY_MESSAGE是返回的值,表示不允許某人進入夜總會。 從本質上講,字符串通常具有更好的描述性,但是請始終檢查您的代碼以找出可以改進的地方。
這里的關鍵是減少代碼閱讀器嘗試解析代碼行所花費的時間。
5.難以讀取的測試名稱
@Test void testingNumberOneAndNumberTwoCanBeAddedTogetherToProduceNumberThree() {... }您花了多長時間閱讀以上內容? 它易于閱讀嗎?您能快速了解一下此處正在測試的內容嗎?還是需要解析許多字符?
幸運的是,我們可以嘗試以更好的方式命名測試,方法是將測試減少到實際測試的水平,并刪除試圖添加的華夫餅:
@Test void twoNumbersCanBeAdded() {... }它的閱讀效果更好嗎? 我們減少了這里的單詞數量,更易于解析。 如果我們可以更進一步,問我們是否可以放棄使用駱駝箱怎么辦:
@Test void two_numbers_can_be_added() {... }這是一個優先事項,應該由對給定代碼庫做出貢獻的人員同意。 使用蛇形小寫字母(如上所述)可以幫助提高測試名稱的可讀性,因為您更可能打算模仿書面句子。 因此,蛇形格的使用緊隨普通書面句子中存在的物理空間。 但是,Java不允許在方法名稱中使用空格,這是我們所擁有的最好的方法,缺少使用Spock之類的東西。
6.依賴項注入的設置器
通常,對于測試,您希望能夠為給定對象(也稱為“協作對象”或簡稱為“協作者”)注入依賴關系。 為了達到這個目的,您可能已經看到了類似以下內容的內容:
@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的模擬,因此我們可以驗證服務和存儲庫之間是否發生了正確的協作。
與圍繞突變的點類似,這里我們對ProductService進行突變,而不是將其構造為所需狀態。 可以通過將協作者注入構造函數中來避免這種情況:
@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); }因此,現在我們將協作者注入了構造函數中,現在我們在構造時就知道對象將處于什么狀態。但是,您可能會問“在此過程中我們是否沒有丟失某些上下文?”。
我們已經從
service.setRepository(repository);至
ProductService service = new ProductService(repository);前者更具描述性。 因此,如果您不喜歡這種上下文丟失的情況,則可以選擇類似構建器的內容,而創建以下內容:
@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()方法記錄協作者注入的情況下改變ProductService 。
7.非描述性驗證
如前所述,您的測試通常會包含驗證語句。 不用自己動手,您通常會利用庫來執行此操作。 但是,您必須注意不要掩蓋驗證的意圖。 要了解我在說什么,請看以下示例。
@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); }現在,如果您看上面的內容,您是否立即知道該斷言表明沒有錯誤顯示給用戶? 可能是因為它是測試的名稱,但是您可能不將該代碼行與測試名稱相關聯 。 這是因為它是Mockito的代碼,并且通用以適應許多不同的用例。 它按照它說的做,檢查與UIComponent的模擬是否沒有交互。
但是,這意味著您的測試有所不同。 我們如何設法使其更加清晰。
@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"); }這樣會更好一些,因為此代碼的讀者有很大的潛力可以快速了解此行的工作。 但是,在某些情況下,可能仍然很難閱讀。 在這種情況下,請按照以下說明提取一種方法,以更好地解釋您的驗證。
@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"); }上面的代碼并不完美,但是在當前測試的范圍內,它肯定可以提供我們正在驗證的內容的高級概述。
結束語
我希望您喜歡這篇文章,并且下次您完成編寫測試時將花費一兩個重構步驟。 在下一次之前,我給你以下報價:
“必須編寫程序供人們閱讀,并且只能偶然地使機器執行。” ― Harold Abelson,計算機程序的結構和解釋
翻譯自: https://www.javacodegeeks.com/2019/08/seven-testing-sins-and-how-to-avoid-them.html
開發罪過
總結
以上是生活随笔為你收集整理的开发罪过_七大罪过与如何避免的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 清除计算机中灰尘和污垢的6种简单方法电脑
- 下一篇: 据说吃鸡坐AutoFull傲风电脑椅不累
