目录
  • 简介
  • 单元测试
    • 表格驱动测试
    • 分组和并行
    • 主测试函数
    • 其他
  • 性能测试
    • 其他选项
  • 示例测试
    • 总结

      简介

      testing是 Go 语言标准库自带的测试库。在 Go 语言中编写测试很简单,只需要遵循 Go 测试的几个约定,与编写正常的 Go 代码没有什么区别。Go 语言中有 3 种类型的测试:单元测试,性能测试,示例测试。下面依次来介绍。

      单元测试

      单元测试又称为功能性测试,是为了测试函数、模块等代码的逻辑是否正确。接下来我们编写一个库,用于将表示罗马数字的字符串和整数互转。罗马数字是由M/D/C/L/X/V/I这几个字符根据一定的规则组合起来表示一个正整数:

      • M=1000,D=500,C=100,L=50,X=10,V=5,I=1;
      • 只能表示 1-3999 范围内的整数,不能表示 0 和负数,不能表示 4000 及以上的整数,不能表示分数和小数(当然有其他复杂的规则来表示这些数字,这里暂不考虑);
      • 每个整数只有一种表示方式,一般情况下,连写的字符表示对应整数相加,例如I=1II=2III=3。但是,十位字符(I/X/C/M)最多出现 3 次,所以不能用IIII表示 4,需要在V左边添加一个I(即IV)来表示,不能用VIIII表示 9,需要使用IX代替。另外五位字符(V/L/D)不能连续出现 2 次,所以不能出现VV,需要用X代替。
      // roman.go
      package roman
      import (
        "bytes"
        "errors"
        "regexp"
      )
      type romanNumPair struct {
        Roman string
        Num   int
      }
      var (
        romanNumParis []romanNumPair
        romanRegex    *regexp.Regexp
      )
      var (
        ErrOutOfRange   = errors.New("out of range")
        ErrInvalidRoman = errors.New("invalid roman")
      )
      func init() {
        romanNumParis = []romanNumPair{
          {"M", 1000},
          {"CM", 900},
          {"D", 500},
          {"CD", 400},
          {"C", 100},
          {"XC", 90},
          {"L", 50},
          {"XL", 40},
          {"X", 10},
          {"IX", 9},
          {"V", 5},
          {"IV", 4},
          {"I", 1},
        }
        romanRegex = regexp.MustCompile(`^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$`)
      }
      func ToRoman(n int) (string, error) {
        if n <= 0 || n >= 4000 {
          return "", ErrOutOfRange
        }
        var buf bytes.Buffer
        for _, pair := range romanNumParis {
          for n > pair.Num {
            buf.WriteString(pair.Roman)
            n -= pair.Num
          }
        }
        return buf.String(), nil
      }
      func FromRoman(roman string) (int, error) {
        if !romanRegex.MatchString(roman) {
          return 0, ErrInvalidRoman
        }
        var result int
        var index int
        for _, pair := range romanNumParis {
          for roman[index:index+len(pair.Roman)] == pair.Roman {
            result += pair.Num
            index += len(pair.Roman)
          }
        }
        return result, nil
      }

      在 Go 中编写测试很简单,只需要在待测试功能所在文件的同级目录中创建一个以_test.go结尾的文件。在该文件中,我们可以编写一个个测试函数。测试函数名必须是TestXxxx这个形式,而且Xxxx必须以大写字母开头,另外函数带有一个*testing.T类型的参数:

      // roman_test.go
      package roman
      import (
        "testing"
      )
      func TestToRoman(t *testing.T) {
        _, err1 := ToRoman(0)
        if err1 != ErrOutOfRange {
          t.Errorf("ToRoman(0) expect error:%v got:%v", ErrOutOfRange, err1)
        }
        roman2, err2 := ToRoman(1)
        if err2 != nil {
          t.Errorf("ToRoman(1) expect nil error, got:%v", err2)
        }
        if roman2 != "I" {
          t.Errorf("ToRoman(1) expect:%s got:%s", "I", roman2)
        }
      }

      在测试函数中编写的代码与正常的代码没有什么不同,调用相应的函数,返回结果,判断结果与预期是否一致,如果不一致则调用testing.TErrorf()输出错误信息。运行测试时,这些错误信息会被收集起来,运行结束后统一输出。

      测试编写完成之后,使用go test命令运行测试,输出结果:

      $ go test
      

      — FAIL: TestToRoman (0.00s)
          roman_test.go:18: ToRoman(1) expect:I got:
      FAIL
      exit status 1
      FAIL    github.com/darjun/go-daily-lib/testing  0.172s

      我故意将ToRoman()函数中写错了一行代码,n > pair.Num>应该为>=,单元测试成功找出了错误。修改之后重新运行测试:

      $ go test
      PASS
      ok      github.com/darjun/go-daily-lib/testing  0.178s

      这次测试都通过了!

      我们还可以给go test命令传入-v选项,输出详细的测试信息:

      $ go test -v
      

      === RUN   TestToRoman
      — PASS: TestToRoman (0.00s)
      PASS
      ok      github.com/darjun/go-daily-lib/testing  0.174s

      在运行每个测试函数前,都输出一行=== RUN,运行结束之后输出--- PASS--- FAIL信息。

      表格驱动测试

      在上面的例子中,我们实际上只测试了两种情况,0 和 1。按照这种方式将每种情况都写出来就太繁琐了,Go 中流行使用表格的方式将各个测试数据和结果列举出来:

      func TestToRoman(t *testing.T) {
        testCases := []struct {
          num    int
          expect string
          err    error
        }{
          {0, "", ErrOutOfRange},
          {1, "I", nil},
          {2, "II", nil},
          {3, "III", nil},
          {4, "IV", nil},
          {5, "V", nil},
          {6, "VI", nil},
          {7, "VII", nil},
          {8, "VIII", nil},
          {9, "IX", nil},
          {10, "X", nil},
          {50, "L", nil},
          {100, "C", nil},
          {500, "D", nil},
          {1000, "M", nil},
          {31, "XXXI", nil},
          {148, "CXLVIII", nil},
          {294, "CCXCIV", nil},
          {312, "CCCXII", nil},
          {421, "CDXXI", nil},
          {528, "DXXVIII", nil},
          {621, "DCXXI", nil},
          {782, "DCCLXXXII", nil},
          {870, "DCCCLXX", nil},
          {941, "CMXLI", nil},
          {1043, "MXLIII", nil},
          {1110, "MCX", nil},
          {1226, "MCCXXVI", nil},
          {1301, "MCCCI", nil},
          {1485, "MCDLXXXV", nil},
          {1509, "MDIX", nil},
          {1607, "MDCVII", nil},
          {1754, "MDCCLIV", nil},
          {1832, "MDCCCXXXII", nil},
          {1993, "MCMXCIII", nil},
          {2074, "MMLXXIV", nil},
          {2152, "MMCLII", nil},
          {2212, "MMCCXII", nil},
          {2343, "MMCCCXLIII", nil},
          {2499, "MMCDXCIX", nil},
          {2574, "MMDLXXIV", nil},
          {2646, "MMDCXLVI", nil},
          {2723, "MMDCCXXIII", nil},
          {2892, "MMDCCCXCII", nil},
          {2975, "MMCMLXXV", nil},
          {3051, "MMMLI", nil},
          {3185, "MMMCLXXXV", nil},
          {3250, "MMMCCL", nil},
          {3313, "MMMCCCXIII", nil},
          {3408, "MMMCDVIII", nil},
          {3501, "MMMDI", nil},
          {3610, "MMMDCX", nil},
          {3743, "MMMDCCXLIII", nil},
          {3844, "MMMDCCCXLIV", nil},
          {3888, "MMMDCCCLXXXVIII", nil},
          {3940, "MMMCMXL", nil},
          {3999, "MMMCMXCIX", nil},
          {4000, "", ErrOutOfRange},
        }
        for _, testCase := range testCases {
          got, err := ToRoman(testCase.num)
          if got != testCase.expect {
            t.Errorf("ToRoman(%d) expect:%s got:%s", testCase.num, testCase.expect, got)
          }
          if err != testCase.err {
            t.Errorf("ToRoman(%d) expect error:%v got:%v", testCase.num, testCase.err, err)
          }
        }
      }

      上面将要测试的每种情况列举出来,然后针对每个整数调用ToRoman()函数,比较返回的罗马数字字符串和错误值是否与预期的相符。后续要添加新的测试用例也很方便。

      分组和并行

      有时候对同一个函数有不同维度的测试,将这些组合在一起有利于维护。例如上面对ToRoman()函数的测试可以分为非法值,单个罗马字符和普通 3 种情况。

      为了分组,我对代码做了一定程度的重构,首先抽象一个toRomanCase结构:

      type toRomanCase struct {
        num    int
        expect string
        err    error
      }

      将所有的测试数据划分到 3 个组中:

      var (
        toRomanInvalidCases []toRomanCase
        toRomanSingleCases  []toRomanCase
        toRomanNormalCases  []toRomanCase
      )
      func init() {
        toRomanInvalidCases = []toRomanCase{
          {0, "", roman.ErrOutOfRange},
          {4000, "", roman.ErrOutOfRange},
        }
        toRomanSingleCases = []toRomanCase{
          {1, "I", nil},
          {5, "V", nil},
          // ...
        }
        toRomanNormalCases = []toRomanCase{
          {2, "II", nil},
          {3, "III", nil},
          // ...
        }
      }

      然后为了避免代码重复,抽象一个运行多个toRomanCase的函数:

      func testToRomanCases(cases []toRomanCase, t *testing.T) {
        for _, testCase := range cases {
          got, err := roman.ToRoman(testCase.num)
          if got != testCase.expect {
            t.Errorf("ToRoman(%d) expect:%s got:%s", testCase.num, testCase.expect, got)
          }
          if err != testCase.err {
            t.Errorf("ToRoman(%d) expect error:%v got:%v", testCase.num, testCase.err, err)
          }
        }
      }

      为每个分组定义一个测试函数:

      func testToRomanInvalid(t *testing.T) {
        testToRomanCases(toRomanInvalidCases, t)
      }
      func testToRomanSingle(t *testing.T) {
        testToRomanCases(toRomanSingleCases, t)
      }
      func testToRomanNormal(t *testing.T) {
        testToRomanCases(toRomanNormalCases, t)
      }

      在原来的测试函数中,调用t.Run()运行不同分组的测试函数,t.Run()第一个参数为子测试名,第二个参数为子测试函数:

      func TestToRoman(t *testing.T) {
        t.Run("Invalid", testToRomanInvalid)
        t.Run("Single", testToRomanSingle)
        t.Run("Normal", testToRomanNormal)
      }

      运行:

      $ go test -v
      

      === RUN   TestToRoman
      === RUN   TestToRoman/Invalid
      === RUN   TestToRoman/Single
      === RUN   TestToRoman/Normal
      — PASS: TestToRoman (0.00s)
          — PASS: TestToRoman/Invalid (0.00s)
          — PASS: TestToRoman/Single (0.00s)
          — PASS: TestToRoman/Normal (0.00s)
      PASS
      ok      github.com/darjun/go-daily-lib/testing  0.188s

      可以看到,依次运行 3 个子测试,子测试名是父测试名和t.Run()指定的名字组合而成的,如TestToRoman/Invalid

      默认情况下,这些测试都是依次顺序执行的。如果各个测试之间没有联系,我们可以让他们并行以加快测试速度。方法也很简单,在testToRomanInvalid/testToRomanSingle/testToRomanNormal这 3 个函数开始处调用t.Parallel(),由于这 3 个函数直接调用了testToRomanCases,也可以只在testToRomanCases函数开头出添加:

      func testToRomanCases(cases []toRomanCase, t *testing.T) {
        t.Parallel()
        // ...
      }

      运行:

      $ go test -v
      ...
      --- PASS: TestToRoman (0.00s)
          --- PASS: TestToRoman/Invalid (0.00s)
          --- PASS: TestToRoman/Normal (0.00s)
          --- PASS: TestToRoman/Single (0.00s)
      PASS
      ok      github.com/darjun/go-daily-lib/testing  0.182s

      我们发现测试完成的顺序并不是我们指定的顺序。

      另外,这个示例中我将roman_test.go文件移到了roman_test包中,所以需要import "github.com/darjun/go-daily-lib/testing/roman"。这种方式在测试包有循环依赖的情况下非常有用,例如标准库中net/http依赖net/urlurl的测试函数依赖net/http,如果把测试放在net/url包中,那么就会导致循环依赖url_test(net/url)->net/http->net/url。这时可以将url_test放在一个独立的包中。

      主测试函数

      有一种特殊的测试函数,函数名为TestMain(),接受一个*testing.M类型的参数。这个函数一般用于在运行所有测试前执行一些初始化逻辑(如创建数据库链接),或所有测试都运行结束之后执行一些清理逻辑(释放数据库链接)。如果测试文件中定义了这个函数,则go test命令会直接运行这个函数,否者go test会创建一个默认的TestMain()函数。这个函数的默认行为就是运行文件中定义的测试。我们自定义TestMain()函数时,也需要手动调用m.Run()方法运行测试函数,否则测试函数不会运行。默认的TestMain()类似下面代码:

      func TestMain(m *testing.M) {
        os.Exit(m.Run())
      }

      下面自定义一个TestMain()函数,打印go test支持的选项:

      func TestMain(m *testing.M) {
        flag.Parse()
        flag.VisitAll(func(f *flag.Flag) {
          fmt.Printf("name:%s usage:%s value:%v\n", f.Name, f.Usage, f.Value)
        })
        os.Exit(m.Run())
      }

      运行:

      $ go test -v
      name:test.bench usage:run only benchmarks matching `regexp` value:
      name:test.benchmem usage:print memory allocations for benchmarks value:false
      name:test.benchtime usage:run each benchmark for duration `d` value:1s
      name:test.blockprofile usage:write a goroutine blocking profile to `file` value:
      name:test.blockprofilerate usage:set blocking profile `rate` (see runtime.SetBlockProfileRate) value:1
      name:test.count usage:run tests and benchmarks `n` times value:1
      name:test.coverprofile usage:write a coverage profile to `file` value:
      name:test.cpu usage:comma-separated `list` of cpu counts to run each test with value:
      name:test.cpuprofile usage:write a cpu profile to `file` value:
      name:test.failfast usage:do not start new tests after the first test failure value:false
      name:test.list usage:list tests, examples, and benchmarks matching `regexp` then exit value:
      name:test.memprofile usage:write an allocation profile to `file` value:
      name:test.memprofilerate usage:set memory allocation profiling `rate` (see runtime.MemProfileRate) value:0
      name:test.mutexprofile usage:write a mutex contention profile to the named file after execution value:
      name:test.mutexprofilefraction usage:if >= 0, calls runtime.SetMutexProfileFraction() value:1
      name:test.outputdir usage:write profiles to `dir` value:
      name:test.paniconexit0 usage:panic on call to os.Exit(0) value:true
      name:test.parallel usage:run at most `n` tests in parallel value:8
      name:test.run usage:run only tests and examples matching `regexp` value:
      name:test.short usage:run smaller test suite to save time value:false
      name:test.testlogfile usage:write test action log to `file` (for use only by cmd/go) value:
      name:test.timeout usage:panic test binary after duration `d` (default 0, timeout disabled) value:10m0s
      name:test.trace usage:write an execution trace to `file` value:
      name:test.v usage:verbose: print additional output value:tru

      这些选项也可以通过go help testflag查看。

      其他

      另一个函数FromRoman()我没有写任何测试,就交给大家了

      声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。