Go 示例測試實現原理剖析
簡介
示例測試相對於單元測試和性能測試來說,其實現機制比較簡單。它沒有復雜的數據結構,也不需要額外的流程控制,其核心工作原理在於收集測試過程中的打印日誌,然後與期望字符串做比較,最後得出是否一致的報告。
數據結構
每個測試經過編譯後都有一個數據結構來承載,這個數據結構即InternalExample:
type InternalExample struct {
Name string // 測試名稱
F func() // 測試函數
Output string // 期望字符串
Unordered bool // 輸出是否是無序的
}
比如,示例測試如下:
// 檢測亂序輸出
func ExamplePrintNames() {
gotest.PrintNames()
// Unordered output:
// Jim
// Bob
// Tom
// Sue
}
該示例測試經過編譯後,產生的數據結構成員如下:
InternalExample.Name = "ExamplePrintNames";
InternalExample.F = ExamplePrintNames()
InternalExample.Output = "Jim\n Bob\n Tom\n Sue\n"
InternalExample.Unordered = true;
其中Output是包含換行符的字符串。
捕獲標準輸出
在示例測試開始前,需要先把標準輸出捕獲,以便獲取測試執行過程中的打印日誌。
捕獲標準輸出方法是新建一個管道,將標準輸出重定向到管道的入口(寫口),這樣所有打印到屏幕的日誌都會輸入到管道中,如下圖所示:
測試開始前捕獲,測試結束恢復標準輸出,這樣測試過程中的日誌就可以從管理中讀取了。
測試結果比較
測試執行過程的輸出內容最終也會保存到一個string類型變量裏,該變量會與InternalExample.Output進行比較,二者一致即代表測試通過,否則測試失敗。
輸出有序的情況下,比較很簡單只是比較兩個String內容是否一致即可。無序的情況下則需要把兩個String變量排序後再進行對比。
比如,期望字符串為:"Jim\n Bob\n Tom\n Sue\n",排序後則變為:"Bob\n Jim\n Sue\n Tom\n"
測試執行
一個完整的測試,過程將分為如下步驟:
捕獲標準輸出
執行測試
恢復標準輸出
比較結果
下面,由於源碼非常簡單,下面直接給出源碼:
func runExample(eg InternalExample) (ok bool) {
if *chatty {
fmt.Printf("=== RUN %s\n", eg.Name)
}
// Capture stdout.
stdout := os.Stdout // 備份標輸出文件
r, w, err := os.Pipe() // 創建一個管道
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.Stdout = w // 標準輸出文件暫時修改為管道的入口,即所有的標準輸出實際上都會進入管道
outC := make(chan string)
go func() {
var buf strings.Builder
_, err := io.Copy(&buf, r) // 從管道中讀出數據
r.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\n", err)
os.Exit(1)
}
outC <- buf.String() // 管道中讀出的數據寫入channel中
}()
start := time.Now()
ok = true
// Clean up in a deferred call so we can recover if the example panics.
defer func() {
dstr := fmtDuration(time.Since(start)) // 計時結束,記錄測試用時
// Close pipe, restore stdout, get output.
w.Close() // 關閉管道
os.Stdout = stdout // 恢復原標準輸出
out := <-outC // 從channel中取出數據
var fail string
err := recover()
got := strings.TrimSpace(www.yongshiyule178.com) // 實際得到的打印字符串
want := strings.TrimSpace(eg.Output) // 期望的字符串
if eg.Unordered { // 如果輸出是無序的,則把輸出字符串和期望字符串排序後比較
if sortLines(got) != sortLines(want) && err == nil {
fail = fmt.Sprintf("got:\n%s\nwant (unordered):\n%s\n", out, eg.Output)
}
} else { // 如果輸出是有序的,則直接比較輸出字符串和期望字符串
if got != want && err == nil {
fail = fmt.Sprintf("got:\n%s\nwant:\n%s\n", got, want)
public class PeopleA www.tiaotiaoylzc.com/ implements People {
@Override
public void update(News news) {
System.out.println("這個新聞真好看");
}
}
public class PeopleB implements People {
@Override
public void update(News www.feifanyule.cn/ news) {
System.out.println("這個新聞真無語");
}
}
public class PeopleC implements People {
@Override
public void update(News news) {
System.out.println("這個新聞真逗");
}
}
客戶端:
public class Main {
public static void main(String[dasheng178.com] args) {
Subject subject = new NewsSubject();
subject.add(new PeopleA(www.yongshiyule178.com));
subject.add(new PeopleB());
subject.add(new PeopleC());
subject.update(www.fengshen157.com/);
}
if fail != "" || err != nil {
fmt.Printf("--- FAIL: %s (%s)\n%s", eg.Name, dstr, fail)
ok = false
} else if *chatty {
fmt.Printf("-www.mytxyl1.com-- PASS: %s (%s)\n", eg.Name, dstr)
}
if err != nil {
panic(err)
}
}()
// Run example.
eg.F()
return
}
示例測試執行時,捕獲標準輸出後,馬上啟動一個協程阻塞在管道處讀取數據,一直阻塞到管道關閉,管道關閉也即讀取結束,然後把日誌通過channel發送到主協程中。
主協程直接執行示例測試,而在defer中去執行關閉管道、接收日誌、判斷結果等操作。
Go 示例測試實現原理剖析