• 简介
  • 数据结构
  • 捕获标准输出
  • 测试结果比较
  • 测试执行

    简介

    示例测试相对于单元测试和性能测试来说,其实现机制比较简单。它没有复杂的数据结构,也不需要额外的流程控制,其核心工作原理在于收集测试过程中的打印日志,然后与期望字符串做比较,最后得出是否一致的报告。

    数据结构

    每个测试经过编译后都有一个数据结构来承载,这个数据结构即InternalExample:

    1. type InternalExample struct {
    2. Name string // 测试名称
    3. F func() // 测试函数
    4. Output string // 期望字符串
    5. Unordered bool // 输出是否是无序的
    6. }

    比如,示例测试如下:

    1. // 检测乱序输出
    2. func ExamplePrintNames() {
    3. gotest.PrintNames()
    4. // Unordered output:
    5. // Jim
    6. // Bob
    7. // Tom
    8. // Sue
    9. }

    该示例测试经过编译后,产生的数据结构成员如下:

    • InternalExample.Name = “ExamplePrintNames”;
    • InternalExample.F = ExamplePrintNames()
    • InternalExample.Output = “Jim\n Bob\n Tom\n Sue\n”
    • InternalExample.Unordered = true;

    其中Output是包含换行符的字符串。

    捕获标准输出

    在示例测试开始前,需要先把标准输出捕获,以便获取测试执行过程中的打印日志。

    捕获标准输出方法是新建一个管道,将标准输出重定向到管道的入口(写口),这样所有打印到屏幕的日志都会输入到管道中,如下图所示:

    7.3.5 示例测试实现原理 - 图1

    测试开始前捕获,测试结束恢复标准输出,这样测试过程中的日志就可以从管理中读取了。

    测试结果比较

    测试执行过程的输出内容最终也会保存到一个string类型变量里,该变量会与InternalExample.Output进行比较,二者一致即代表测试通过,否则测试失败。

    输出有序的情况下,比较很简单只是比较两个String内容是否一致即可。无序的情况下则需要把两个String变量排序后再进行对比。

    比如,期望字符串为:”Jim\n Bob\n Tom\n Sue\n”,排序后则变为:”Bob\n Jim\n Sue\n Tom\n”

    测试执行

    一个完整的测试,过程将分为如下步骤:

    1. 捕获标准输出
    2. 执行测试
    3. 恢复标准输出
    4. 比较结果

    下面,由于源码非常简单,下面直接给出源码:

    1. func runExample(eg InternalExample) (ok bool) {
    2. if *chatty {
    3. fmt.Printf("=== RUN %s\n", eg.Name)
    4. }
    5. // Capture stdout.
    6. stdout := os.Stdout // 备份标输出文件
    7. r, w, err := os.Pipe() // 创建一个管道
    8. if err != nil {
    9. fmt.Fprintln(os.Stderr, err)
    10. os.Exit(1)
    11. }
    12. os.Stdout = w // 标准输出文件暂时修改为管道的入口,即所有的标准输出实际上都会进入管道
    13. outC := make(chan string)
    14. go func() {
    15. var buf strings.Builder
    16. _, err := io.Copy(&buf, r) // 从管道中读出数据
    17. r.Close()
    18. if err != nil {
    19. fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\n", err)
    20. os.Exit(1)
    21. }
    22. outC <- buf.String() // 管道中读出的数据写入channel中
    23. }()
    24. start := time.Now()
    25. ok = true
    26. // Clean up in a deferred call so we can recover if the example panics.
    27. defer func() {
    28. dstr := fmtDuration(time.Since(start)) // 计时结束,记录测试用时
    29. // Close pipe, restore stdout, get output.
    30. w.Close() // 关闭管道
    31. os.Stdout = stdout // 恢复原标准输出
    32. out := <-outC // 从channel中取出数据
    33. var fail string
    34. err := recover()
    35. got := strings.TrimSpace(out) // 实际得到的打印字符串
    36. want := strings.TrimSpace(eg.Output) // 期望的字符串
    37. if eg.Unordered { // 如果输出是无序的,则把输出字符串和期望字符串排序后比较
    38. if sortLines(got) != sortLines(want) && err == nil {
    39. fail = fmt.Sprintf("got:\n%s\nwant (unordered):\n%s\n", out, eg.Output)
    40. }
    41. } else { // 如果输出是有序的,则直接比较输出字符串和期望字符串
    42. if got != want && err == nil {
    43. fail = fmt.Sprintf("got:\n%s\nwant:\n%s\n", got, want)
    44. }
    45. }
    46. if fail != "" || err != nil {
    47. fmt.Printf("--- FAIL: %s (%s)\n%s", eg.Name, dstr, fail)
    48. ok = false
    49. } else if *chatty {
    50. fmt.Printf("--- PASS: %s (%s)\n", eg.Name, dstr)
    51. }
    52. if err != nil {
    53. panic(err)
    54. }
    55. }()
    56. // Run example.
    57. eg.F()
    58. return
    59. }

    示例测试执行时,捕获标准输出后,马上启动一个协程阻塞在管道处读取数据,一直阻塞到管道关闭,管道关闭也即读取结束,然后把日志通过channel发送到主协程中。

    主协程直接执行示例测试,而在defer中去执行关闭管道、接收日志、判断结果等操作。