編寫難於測試的程式碼的5種方式
本文由碼農網 – 孫騰浩原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃!
有一次,我在一個講座上聽到主持人問聽眾如何故意編寫難於測試的程式碼。在場的小夥伴都驚呆了,因為沒有任何人會故意寫這種糟糕的程式碼。我記得他們甚至給不出一個好的答案。
當然,這個問題的目的不在於教大家如何寫使同事欲哭無淚的爛程式碼。而是為了瞭解什麼樣的程式碼難於測試,來避免這些嚴重的問題。
這裡給出我對上面那個問題的答案(當然這只是我的個人觀點,每個人討厭的都不盡相同。)
1.用大量的靜態欄位
尤其是在不同類中共享靜態的集合類,比如下面這個:
public class Catalog { private static List<Person> people = new ArrayList<>(); public void addPerson(Person person) { if (Calendar.getInstance().get(Calendar.YEAR) - person.getYearOfBirth() < 18) { throw new IllegalArgumentException("Only adults admitted."); } people.add(person); } public int getNrOfPeople() { return people.size(); } }
現在我們來看看測試程式碼:
public class CatalogTest { private final Catalog underTest = new Catalog(); @Test(expected=IllegalArgumentException.class) public void addingAMinorShouldThrowException() { assertEquals(0, underTest.getNrOfPeople()); // precondition Person p = new Person(2015); underTest.addPerson(p); } @Test public void addingAnAdultShouldSucceed() { assertEquals(0, underTest.getNrOfPeople()); // precondition, or is it? Person p = new Person(1985); underTest.addPerson(p); assertEquals(1, underTest.getNrOfPeople()); } }
如果你執行這個兩個測試,你會發現期待丟擲異常的那個用例失敗了。這有些讓你懷疑人生了,但是JUnit可以自由安排用例執行順序而不依賴於編寫用例的順序。在這段程式碼中第二個測試用例先執行,它檢測集合是空的,然後成功註冊了一個adult。由於我們的集合是靜態的,第一個測試用例檢測到集合不是空的(我們在之前的測試用例已經放進去一個people了),所以失敗了。
一旦我們刪掉static關鍵字,兩個測試用例都成功執行。在每個測試用例執行前,JUnit會將測試用例中的欄位初始化(不僅僅是@Before註解方法中的欄位)。現在我們有一個例項成員,而不是一個繫結在類上的靜態people列表。這意味著每個測試用例執行前都會建立一個新的Catalog物件,包含一個新的列表。每次我們都有一個新的空people列表。
當然,在這個例子中我們很容易發覺並解決這個問題,但想象一個龐大的系統中,有眾多類操作的people列表。
靜態的可變集合(據我同事所說)就像一個垃圾桶,充斥著各種垃圾,真應該避免使用。
2.宣告眾多final類
一個類宣告final就阻止了被mock。下面就是嘗試用Mockito去mock一個final類會發生什麼:
org.mockito.exceptions.base.MockitoException: Cannot mock/spy class <CLASS_NAME> Mockito cannot mock/spy following: - final classes - anonymous classes - primitive types at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl...
一個類宣告瞭final會有嚴重後果(比如它不能被繼承和mock),所以很有理由不這樣做。
等等,不要放棄治療。面對一個final類,只需要一些其他努力。Powermock就可以mock這樣的類,不過我覺得這個類庫治標不治本。
另一種方法是建立一個非fianl的包裹類,包裹住final類然後mock這個包裹類。 當然,前提是你能避免可能的類簽名的改變。
3.總是在方法中建立物件,尤其是在建構函式中。
我認為這不需要過多解釋。在方法或建構函式中建立物件讓我們無法引入測試的複製品。將依賴關係完全硬編碼,讓我們無法mock,寫不出真正的單元測試(排除一切外部依賴,在獨立環境下對一個類快速測試)。
依賴應該被注入,主要通過建構函式。如果因為一些原因做不到這點,建立物件的工作應該放到一個protected的方法中,這樣一來繼承它的虛構類可以重寫該方法。
4.方法/測試的名字和內容永遠不一致
很多人認為長方法(long methods)是測試的頭號公敵。儘管很多情況下是這樣,但不絕對。想象一個小巧的程式碼很長的求平方根函式。它接受一個整型,返回一個浮點數。因為我們很清楚平方根怎麼求,所以不需要關心程式碼實現的細節。我們把這個方法當做黑盒,來測一些顯而易見的值(9,25,36)和一些不常見的值。
然而,如果這個方法名叫log(數學裡的log),那麼我們要寫的測試就驢脣不對馬嘴了。這簡直是在浪費時間,所寫的測試用例完全沒有用。
測試方法也同樣。一旦測試失敗,描述測試工作的測試名真的很有用了。比如測試名是throwsExceptionForNegativeNumbers,測試了一個正數並且沒有任何異常,這對我們明白我們在測試什麼很有幫助。
5.從來不把流操作分成若干指令
因為Java 8 的streams有流暢的介面,這並不意味著filter,map,flatMap和其他操作一個接著一個鏈式呼叫(或者巢狀呼叫)。
讓我們看個例子。每個football club(足球俱樂部)提供一個soccer players(足球運動員)的列表,返回25歲到30歲間的前鋒球員。
public Map<String, Integer> someMethodName(List<Player> players) { return players.stream().filter(player -> { int now = Calendar.getInstance().get(Calendar.YEAR); return (now - player.getYearOfBirth() < 30) && (now - player.getYearOfBirth() > 25); }).filter(player -> player.getPosition() == Position.ST) .collect(Collectors.groupingBy(Player::getPlaysFor, Collectors.summingLong(Player::getValue))); }
這裡的問題是很難知道什麼樣的資料應該通過這樣的鏈式方法,所以要遍歷所有可能的資料路由。很難指出我們要用什麼樣的players(運動員)列表作為輸入 。
一長串的操作雖然很吸引人,但可讀性很差。一般來說,根據整潔程式碼規則,把它們拆分成程式碼塊,提取成變數或方法是個好主意。
經過一些提取,程式碼重構如下
public Map<String, Integer> someMethodName(List<Player> players) { Stream<Player> playersInAgeRange = players.stream().filter(isPlayerInAgeRange()); Stream<Player> strikers = playersInAgeRange.filter(isPlayerStriker()); return strikers.collect(Collectors.groupingBy(Player::getPlaysFor, Collectors.summingLong(Player::getValue))); } private Predicate<? super Player> isPlayerStriker() { return player -> player.getPosition() == Position.ST; } private Predicate<? super Player> isPlayerInAgeRange() { return player -> { int now = Calendar.getInstance().get(Calendar.YEAR); return (now - player.getYearOfBirth() < 30) && (now - player.getYearOfBirth() > 25); }; }
儘管程式碼有些長,但可讀性大大提高。
譯文連結:http://www.codeceo.com/article/5-ways-write-hard-to-test-code.html
英文原文:5 Easy Ways to Write Hard-to-Test Code
翻譯作者:碼農網 – 孫騰浩
[ 轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]
相關文章
- 編寫可測試的 JavaSript 程式碼Java
- 編寫可測試的 JavaScript 程式碼JavaScript
- 如何編寫優秀的測試程式碼|單元測試
- .NET Core TDD 前傳: 編寫易於測試的程式碼 -- 縫
- 使用 xunit 編寫測試程式碼
- 使用 intern 編寫測試程式碼
- 前端進階-編寫測試程式碼前端
- .NET Core TDD 前傳: 編寫易於測試的程式碼 -- 依賴項
- .NET Core TDD 前傳: 編寫易於測試的程式碼 -- 構建物件物件
- 為程式碼編寫穩定的單元測試 [Go]Go
- .NET Core TDD 前傳: 編寫易於測試的程式碼 -- 單一職責
- .NET Core TDD 前傳: 編寫易於測試的程式碼 -- 全域性狀態
- 編寫易於理解的程式碼
- JUnit5編寫基本測試
- Vue3,用組合的方式來編寫更好的程式碼(1/5)Vue
- Javascript事件處理程式的5種方式(相容寫法)JavaScript事件
- 程式碼寫作測試
- 編寫可測試的Javascript程式碼(1):反模式及其解決方案JavaScript模式
- 如何編寫測試團隊通用的Jmeter指令碼JMeter指令碼
- 無需編寫程式碼,API業務流程測試,零程式碼實現API
- LoadRunner測試WebService的3種方式Web
- 寫 Laravel 測試程式碼 (五)Laravel
- 寫 Laravel 測試程式碼 (二)Laravel
- 編寫可測試的Javascript程式碼(2):從反模式進行重構JavaScript模式
- 關於寫非同步程式碼測試用例的一些思考非同步
- 用LoadRunner編寫socket應用的測試指令碼指令碼
- 測試工程師必備:掌握這5種設計方法快速編寫測試用例~思路分析工程師
- 「乾貨」5種最常見的移動應用程式測試錯誤方式,如何規避?
- 自己編寫的(測試點總結)
- 程式碼迭代的幾種方式
- Java中測試異常的多種方式Java
- 一個新手為老程式碼寫測試程式的心得
- 你們現在編寫程式碼是從先寫測試開始嗎?
- 如何用 JMeter 編寫效能測試指令碼?JMeter指令碼
- Java中建立並寫檔案的5種方式Java
- 效能測試——壓測工具locust——指令碼初步編寫指令碼
- 關於圓角的5種處理方式
- 用Jmeter編寫一個較複雜的測試指令碼JMeter指令碼