go test - goconvey

GoConvey 简介

GoConvey 网站 : https://smartystreets.github.io/goconvey

兼容 Go 内置 testing库,丰富的断言函数,彩色的控制台输出,提供命令行工具,更加直观的 Web 界面,测试会自动运行

1
go install github.com/smartystreets/goconvey@latest

运行 goconvey ,可以在本地运行一个强大 Web 图形界面,配合内置的 testing 使用,以提高开发效率

基本使用

产品代码

实现判断两个字符串切片是否相等的函数 StringSliceEqual,主要逻辑包括:

  • 两个字符串切片长度不相等时,返回 false
  • 两个字符串切片一个是 nil,另一个不是 nil 时,返回 false
  • 遍历两个切片,比较对应索引的两个切片元素值,如果不相等,返回 false
  • 否则,返回 true
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func StringSliceEqual(a, b []string) bool {
    if len(a) != len(b) {
        return false
    }

    if (a == nil) != (b == nil) {
        return false
    }

    for i, v := range a {
        if v != b[i] {
            return false
        }
    }
    return true
}
1
2
3
if (a == nil) != (b == nil) {
    return false
}

实例化a和b,[]string{} 和 []string(nil),这时两个字符串切片的长度都是 0,但不相等

测试代码

正常情况的测试用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqual should return true when a != nil  && b != nil", t, func() {
        a := []string{"hello", "goconvey"}
        b := []string{"hello", "goconvey"}
        So(StringSliceEqual(a, b), ShouldBeTrue)
    })
}
1
go test -v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
=== RUN   TestStringSliceEqual

  TestStringSliceEqual should return true when a != nil  && b != nil ✔


1 total assertion

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok  	example	0.002s

So(StringSliceEqual(a, b), ShouldBeFalse) 故意不过后

1
go test -v -trimpath
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
=== RUN   TestStringSliceEqual

  TestStringSliceEqual should return true when a != nil  && b != nil ✘


Failures:

  * example/slice_test.go 
  Line 13:
  Expected: false
  Actual:   true


1 total assertion

--- FAIL: TestStringSliceEqual (0.00s)
FAIL
exit status 1
FAIL	example	0.003s

-trimpath 可去本地目录信息

再补充 3 个测试用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqual should return true when a != nil  && b != nil", t, func() {
        a := []string{"hello", "goconvey"}
        b := []string{"hello", "goconvey"}
        So(StringSliceEqual(a, b), ShouldBeTrue)
    })

    Convey("TestStringSliceEqual should return true when a == nil  && b == nil", t, func() {
        So(StringSliceEqual(nil, nil), ShouldBeTrue)
    })

    Convey("TestStringSliceEqual should return false when a == nil  && b != nil", t, func() {
        a := []string(nil)
        b := []string{}
        So(StringSliceEqual(a, b), ShouldBeFalse)
    })

    Convey("TestStringSliceEqual should return false when a != nil  && b != nil", t, func() {
        a := []string{"hello", "world"}
        b := []string{"hello", "goconvey"}
        So(StringSliceEqual(a, b), ShouldBeFalse)
    })
}

每一个 Convey 语句对应一个测试用例,一个函数的多个测试用例可以通过一个测试函数的多个 Convey 语句来呈现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
=== RUN   TestStringSliceEqual

  TestStringSliceEqual should return true when a != nil  && b != nil ✔


1 total assertion


  TestStringSliceEqual should return true when a == nil  && b == nil ✔


2 total assertions


  TestStringSliceEqual should return false when a == nil  && b != nil ✔


3 total assertions


  TestStringSliceEqual should return false when a != nil  && b != nil ✔


4 total assertions

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok  	example	0.003s

Convey 语句的嵌套

Convey 语句可以无限嵌套,以体现测试用例之间的关系。只有最外层的 Convey 需要传入 *testing.T 类型的变量 t

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqual", t, func() {
        Convey("should return true when a != nil  && b != nil", func() {
            a := []string{"hello", "goconvey"}
            b := []string{"hello", "goconvey"}
            So(StringSliceEqual(a, b), ShouldBeTrue)
        })

        Convey("should return true when a == nil  && b == nil", func() {
            So(StringSliceEqual(nil, nil), ShouldBeTrue)
        })

        Convey("should return false when a == nil  && b != nil", func() {
            a := []string(nil)
            b := []string{}
            So(StringSliceEqual(a, b), ShouldBeFalse)
        })

        Convey("should return false when a != nil  && b != nil", func() {
            a := []string{"hello", "world"}
            b := []string{"hello", "goconvey"}
            So(StringSliceEqual(a, b), ShouldBeFalse)
        })
    })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
=== RUN   TestStringSliceEqual

  TestStringSliceEqual 
    should return true when a != nil  && b != nil ✔
    should return true when a == nil  && b == nil ✔
    should return false when a == nil  && b != nil ✔
    should return false when a != nil  && b != nil ✔


4 total assertions

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok  	example	0.003s

BDD 风格

一种三层嵌套的惯用法,核心点是通过 GWT(Given…When…Then)格式来描述测试用例,示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func TestStringSliceEqualIfBothNotNil(t *testing.T) {
    Convey("Given two string slice which are both not nil", t, func() {
       a := []string{"hello", "goconvey"}
       b := []string{"hello", "goconvey"}
       Convey("When the comparision is done", func() {
          result := StringSliceEqual(a, b)
          Convey("Then the result should be true", func() {
             So(result, ShouldBeTrue)
          })
       })
    })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
=== RUN   TestStringSliceEqualIfBothNotNil

  Given two string slice which are both not nil 
    When the comparision is done 
      Then the result should be true ✔


1 total assertion

--- PASS: TestStringSliceEqualIfBothNotNil (0.00s)
PASS
ok  	example	0.003s

按 GWT 格式写测试用例时,每一组 GWT 对应一条测试用例,即最内层的 Convey 语句不像两层嵌套时可以有多个,而是只能有一个 Convey 语句

使用测试套的形式,一个测试函数包含多条用例,每条用例使用 Convey 语句四层嵌套的惯用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
func TestStringSliceEqual(t *testing.T) {
    Convey("TestStringSliceEqualIfBothNotNil", t, func() {
        Convey("Given two string slice which are both not nil", func() {
            a := []string{"hello", "goconvey"}
            b := []string{"hello", "goconvey"}
            Convey("When the comparision is done", func() {
                result := StringSliceEqual(a, b)
                Convey("Then the result should be true", func() {
                    So(result, ShouldBeTrue)
                })
            })
        })
    })

    Convey("TestStringSliceEqualIfBothNil", t, func() {
        Convey("Given two string slice which are both nil", func() {
            var a []string = nil
            var b []string = nil
            Convey("When the comparision is done", func() {
                result := StringSliceEqual(a, b)
                Convey("Then the result should be true", func() {
                    So(result, ShouldBeTrue)
                })
            })
        })
    })

    Convey("TestStringSliceNotEqualIfNotBothNil", t, func() {
        Convey("Given two string slice which are both nil", func() {
            a := []string(nil)
            b := []string{}
            Convey("When the comparision is done", func() {
                result := StringSliceEqual(a, b)
                Convey("Then the result should be false", func() {
                    So(result, ShouldBeFalse)
                })
            })
        })
    })

    Convey("TestStringSliceNotEqualIfBothNotNil", t, func() {
        Convey("Given two string slice which are both not nil", func() {
            a := []string{"hello", "world"}
            b := []string{"hello", "goconvey"}
            Convey("When the comparision is done", func() {
                result := StringSliceEqual(a, b)
                Convey("Then the result should be false", func() {
                    So(result, ShouldBeFalse)
                })
            })
        })
    })

}

断言方法

一般相等类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
So(thing1, ShouldEqual, thing2)
So(thing1, ShouldNotEqual, thing2)
So(thing1, ShouldResemble, thing2)		// 用于数组切片map和结构体相等
So(thing1, ShouldNotResemble, thing2)
So(thing1, ShouldPointTo, thing2)
So(thing1, ShouldNotPointTo, thing2)
So(thing1, ShouldBeNil)
So(thing1, ShouldNotBeNil)
So(thing1, ShouldBeTrue)
So(thing1, ShouldBeFalse)
So(thing1, ShouldBeZeroValue)

数字数量比较类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
So(1, ShouldBeGreaterThan, 0)
So(1, ShouldBeGreaterThanOrEqualTo, 0)
So(1, ShouldBeLessThan, 2)
So(1, ShouldBeLessThanOrEqualTo, 2)
So(1.1, ShouldBeBetween, .8, 1.2)
So(1.1, ShouldNotBeBetween, 2, 3)
So(1.1, ShouldBeBetweenOrEqual, .9, 1.1)
So(1.1, ShouldNotBeBetweenOrEqual, 1000, 2000)
So(1.0, ShouldAlmostEqual, 0.99999999, .0001)   // tolerance is optional; default 0.0000000001
So(1.0, ShouldNotAlmostEqual, 0.9, .0001)

包含类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
So([]int{2, 4, 6}, ShouldContain, 4)
So([]int{2, 4, 6}, ShouldNotContain, 5)
So(4, ShouldBeIn, ...[]int{2, 4, 6})
So(4, ShouldNotBeIn, ...[]int{1, 3, 5})
So([]int{}, ShouldBeEmpty)
So([]int{1}, ShouldNotBeEmpty)
So(map[string]string{"a": "b"}, ShouldContainKey, "a")
So(map[string]string{"a": "b"}, ShouldNotContainKey, "b")
So(map[string]string{"a": "b"}, ShouldNotBeEmpty)
So(map[string]string{}, ShouldBeEmpty)
So(map[string]string{"a": "b"}, ShouldHaveLength, 1) // supports map, slice, chan, and string

字符串类

1
2
3
4
5
6
7
8
So("asdf", ShouldStartWith, "as")
So("asdf", ShouldNotStartWith, "df")
So("asdf", ShouldEndWith, "df")
So("asdf", ShouldNotEndWith, "df")
So("asdf", ShouldContainSubstring, "稍等一下")		// optional 'expected occurences' arguments?
So("asdf", ShouldNotContainSubstring, "er")
So("adsf", ShouldBeBlank)
So("asdf", ShouldNotBeBlank)

panic 类

1
2
3
4
So(func(), ShouldPanic)
So(func(), ShouldNotPanic)
So(func(), ShouldPanicWith, "")		// or errors.New("something")
So(func(), ShouldNotPanicWith, "")	// or errors.New("something")

类型检查类

1
2
So(1, ShouldHaveSameTypeAs, 0)
So(1, ShouldNotHaveSameTypeAs, "asdf")

时间和时间间隔类

1
2
3
4
5
6
7
8
9
So(time.Now(), ShouldHappenBefore, time.Now())
So(time.Now(), ShouldHappenOnOrBefore, time.Now())
So(time.Now(), ShouldHappenAfter, time.Now())
So(time.Now(), ShouldHappenOnOrAfter, time.Now())
So(time.Now(), ShouldHappenBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldNotHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenWithin, duration, time.Now())
So(time.Now(), ShouldNotHappenWithin, duration, time.Now())

要点归纳

  1. import goconvey 包时,前面加点号 “.",以减少冗余的代码
  2. 每个测试用例使用 Convey 语句包裹起来,推荐使用 Convey 语句的嵌套,即一个函数有一个或多个测试函数,一个测试函数嵌套两层、三层或四层 Convey 语句
  3. 使用 GoConvey 框架的 Web 界面特性,作为命令行的补充
  4. 在适当的场景下使用 SkipConvey 函数或 SkipSo 函数
  5. 当测试中有需要时,可以定制断言函数
series: