1. 程式人生 > >Go 示例測試實現原理剖析

Go 示例測試實現原理剖析

試用 date 阻塞 工作原理 編譯 make 一個數 sin 操作

  簡介
  
  示例測試相對於單元測試和性能測試來說,其實現機制比較簡單。它沒有復雜的數據結構,也不需要額外的流程控制,其核心工作原理在於收集測試過程中的打印日誌,然後與期望字符串做比較,最後得出是否一致的報告。
  
  數據結構
  
  每個測試經過編譯後都有一個數據結構來承載,這個數據結構即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 示例測試實現原理剖析