《重構 改善既有程式碼的設計》 讀書筆記(十五)

NewReErWen發表於2020-10-09

第四章 構築測試體系

可靠的測試是安全重構的前提。

4.1 自測試程式碼的價值

一套測試就是一個強大的BUG偵測器,能夠大大縮短查詢BUG所需要的時間。

但我們都很懂,編寫測試程式碼,意味著額外的時間,額外的精力,除非真正地感受到這種方法對程式設計速度的提升,否則自我測試是沒有意義的。(體現不出它的意義)

在通常情況下,我們的測試是手動執行的,如果測試變得自動化,能夠自動告訴錯誤出現在哪裡,那麼測試就會變得有趣且有意義了。

撰寫測試程式碼最有用的實際是在開始編寫之前——編寫測試程式碼程式碼其實就是在問自己,這個功能需要做些什麼。編寫測試程式碼還能使你把注意力集中於介面而非實現。預先寫好的測試程式碼也為你的工作安上一個明確的結束標誌。

在Java中,測試的慣用手法是testing main,意思是每個類都應該有一個用於測試的main()。而另一種做法是,建立一個獨立類用於測試,並在一個框架中執行它,使測試工作更輕鬆。

4.2 JUnit測試框架

從現在起,我們又要開始敲程式碼了。

宣告一點:這本書用的JUnit版本比較古老,而我對JUnit其實不很熟悉,所以大致還是按著書的思路來寫,如果有必要的話,我也許會修改。

任何包含測試程式碼的類都必須繼承測試框架所提供的TestCase類。這個框架運用了設計模式之組合模式(Compoeite),允許你將測試程式碼聚集到測試套件(test suite)中。

組合模式:多用於部分-整體這樣的關係。它的目的在於把一類東西聚集起來,在外表現出同樣的行為介面。(恕我不能很好地講出,具體還是網上查+例項比較好)

在本章中,將會創造一個FileReaderTester類來測試檔案讀取器,它與測試框架的UML類圖如下:

/'線上作圖(UML)網址:
http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
如果要修改的的話,開啟網址後,直接複製上圖片連結(或者貼上下方程式碼)修改即可'/
@startuml
Title '測試框架的組合結構'
class FileReaderTester
namespace junit.framework #DDDDDD {
interface Test
TestCase <|- .FileReaderTester
Test <|.. TestCase
Test <|.. TestSuite
Test <- TestSuite
}
@enduml

在這裡插入圖片描述

下面開始建立FileReaderTester類。

public class FileReaderTester extends TestCase {
	public FileReaderTester(String name) {
		super(name);
	}
}

對於老版本和新版本的JUnit而言,一個很顯著的改變是由繼承變成註解,如果之後我能提得起興趣,可能會把較新版本的JUnit也寫一遍。

這個新建的類必須有一個建構函式,完成之後就可以開始新增測試程式碼了。

首先,要設定測試夾具(test fixture),也就是樣本。在這裡供我們測試的樣本是一個檔案。

Bradman		99.94	52	80	10	6996	334	29
Pollock		60.97	23	41	4	2256	274	7
Headley		60.83	22	40	4	2256	270*	10
Sutcliffe	60.73	54	84	9	4555	194	16

在進一步運用這個檔案之前,需要準備好測試夾具。

在JUnit中的TestCase類提供兩個函式針對此用途:setUp()用來產生相關物件,tearDown()負責刪除它們。

我們需要在我們的測試類中對這兩個方法進行覆寫。(在TestCase類中並沒有為這兩個方法附帶程式碼)

public class FileReaderTester extends TestCase {
	private FileReader input;
	
	public FileReaderTester(String name) {
		super(name);
	}

	@Override
	protected void setUp() throws Exception {
		try {
			input = new FileReader("data.txt");
		} catch (FileNotFoundException e) {
			throw new RuntimeException("unable to open test file.");
		}
	}

	@Override
	protected void tearDown() throws Exception {
		try {
			input.close();
		} catch (IOException e) {
			throw new RuntimeException("error on closing test file.");
		}
	}
}

讓我們看看setUp()和tearDown()在jdk中的註釋吧。

/**
 * Sets up the fixture, for example, open a network connection.
 * This method is called before a test is executed.
 * 設定裝置,例如,開啟網路連線。
 * 在執行測試之前呼叫此方法。
 * (簡單說,就是在測試開始前先執行的一段程式碼,可以簡單理解為構造器的作用)
 */
protected void setUp() throws Exception {
}
/**
 * Tears down the fixture, for example, close a network connection.
 * This method is called after a test is executed.
 * 拆下裝置,例如,關閉網路連線。
 * 此方法在執行測試後呼叫。
 * (擦屁股的一個類,會在最後執行,類似於finally)
 */
protected void tearDown() throws Exception {
}

現在測試類準備就緒了,開始編寫測試程式碼。

首先測試read():讀取一些字元,然後檢查後續讀取的字元是否正確。這些測試方法都放在剛才我們建立的FileReaderTester類中。

public void testRead() throws IOException {
	char ch = '&';
	for (int i = 0; i < 4; i ++)
		ch = (char) input.read();
	// 斷言
	assert ('d' == ch);
}

assert()扮演自動測試角色。(這是這本書之前提到的斷言,我不熟悉這個東西)

如果assert()引數值為true,一切安好;否則我們就會收到錯誤通知。

下面,我們要讓這個測試執行起來。

先在測試類中創造一個測試套件。

public static Test suite() {
	TestSuite suite = new TestSuite();
	suite.addTest(new FileReaderTester("testRead"));
	return suite;
}

在我一開始看的時候,其實是有一些懵的,不僅不知道它的作用,也不知道這麼寫為什麼沒有報錯,但是可以回過頭看看之前的那個類圖,這些繼承關係使得它不報錯。

閱讀這部分的原始碼(Test、TestSuite和TestCase),也許能更好地理解組合模式。

在這個測試套件(指代返回的TestSuite物件)中,只含有一個測試用例物件,即FileReaderTester例項。在增加測試用例時,我把待測函式的名稱以字串形式傳給建構函式,從而建立一個物件,用以測試被指定的函式。這裡是利用了Java的反射機制和物件關聯,有興趣可自行研究JUnit原始碼。

如果要讓測試跑起來,需要一個獨立的TestRunner類。

你可以寫一個GUI,也可以用控制檯。此處不用GUI。

public static void main(String[] args) {
	junit.textui.TestRunner.run(suite());
}

執行起來即可。

提供suite.addTest()方法我們可以新增多個測試類,然後去統一執行測試,這就讓我們的測試更加系統化。

--------這一段我用eclipse測試時不會報錯-begin--------

public void testRead() throws IOException {
	char ch = '&';
	for (int i = 0; i < 4; i ++)
		ch = (char) input.read();
	// 斷言 本來此處的ch值應為d,此表示式false
    // 在書中說這裡能顯示一個錯誤資訊,可我這裡沒有
	assert ('2' == ch);
}

--------這一段我用eclipse測試時不會報錯-end--------

書中提到了另一種形式的斷言,這種我這裡可以報錯。

public void testRead() throws IOException {
	char ch = '&';
	for (int i = 0; i < 4; i ++)
		ch = (char) input.read();
	// 斷言
	assertEquals('2', ch);
}

通常情況,建議在寫好測試程式碼時,先故意錯誤——這樣能保證測試機制的確執行了,並且可以報錯。

JUnit還包含一個很好的圖形使用者介面,如果所有測試順利通過,就是綠色,只要有一個測試失敗,就是紅色進度條。這是一個很方便的測試環境。

相關文章