测试框架整理

Golang主要有以下四个测试框架:

  • GoConvey
  • GoStub
  • GoMock
  • Monkey

1、GoConvey简介

测试案例
用于测试的函数,判断两个字符串切片是否相同:

func StringSliceEqual(a, b []string) bool {
    if len(a) != len(b) {
        return false
    }
    // []string{}和[]string(nil),这时两个字符串切片的长度都是0,但不相等
    if (a == nil) != (b == nil) {
        return false
    }
    
    for i, v := range a {
        if v != b[i] {
            return false
        }
    }
    return true
}

测试代码 :

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)
    })
}

执行结果:

=== RUN   TestStringSliceEqual

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


1 total assertion

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok      infra/alg       0.006s

一个函数多用例测试:
1、Convey非嵌套

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)
    })
}

执行结果:

=== 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

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok      infra/alg       0.006s

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

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)
        })
    })
}

执行结果:

=== RUN   TestStringSliceEqual

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

2 total assertions

--- PASS: TestStringSliceEqual (0.00s)
PASS
ok      infra/alg       0.006s

Web界面
GoConvey不仅支持在命令行进行自动化编译测试,而且还支持在 Web 界面进行自动化编译测试。

$GOPATH/bin/goconvey

在这里插入图片描述

Web界面的主要功能:

  1. 可以设置界面主题
  2. 查看完整的测试结果
  3. 使用浏览器提醒等实用功能
  4. 自动检测代码变动并编译测试
  5. 半自动化书写测试用例
  6. 查看测试覆盖率
  7. 临时屏蔽某个包的编译测试

Skip
针对想忽略但又不想删掉或注释掉某些断言操作,GoConvey提供了Convey/So的Skip方法:

  • SkipConvey函数表明相应的闭包函数将不被执行
  • SkipSo函数表明相应的断言将不被执行

当存在SkipConvey或SkipSo时,测试日志中会显式打上"skipped"形式的标记:

  • 当测试代码中存在SkipConvey时,相应闭包函数中不管是否为SkipSo,都将被忽略,测试日志中对应的符号仅为一个"⚠"
  • 当测试代码Convey语句中存在SkipSo时,测试日志中每个So对应一个"✔"或"✘",每个SkipSo对应一个"⚠",按实际顺序排列
  • 不管存在SkipConvey还是SkipSo时,测试日志中都有字符串"{n} total assertions (one or more
    sections skipped)",其中{n}表示测试中实际已运行的断言语句数
摘自:https://www.jianshu.com/p/e3b2b1194830

2、GoStub简介

使用场景

  1. 基本场景:为一个全局变量打桩
  2. 基本场景:为一个函数打桩
  3. 基本场景:为一个过程打桩
  4. 复合场景:由任意相同或不同的基本场景组合而成

1. 为一个全局变量打桩
假设num为被测函数中使用的一个全局整型变量,当前测试用例中假定num的值为150,则打桩的代码如下:

stubs := Stub(&num, 150)
defer stubs.Reset()

stubs是GoStub框架的函数接口Stub返回的对象,该对象有Reset操作,即将全局变量的值恢复为原值。

2. 为一个函数打桩
假设这是我们产品的既有代码中定义的函数:

func Exec(cmd string, args ...string) (string, error) {
    ...
}

则Exec函数是不能通过GoStub框架打桩的。

若要想将Exec函数通过GoStub框架打桩,需要对该函数的声明做重构,即将Exec函数定义为匿名函数,同时将它赋值给Exec变量, 重构后的代码如下:

var Exec = func(cmd string, args ...string) (string, error) {
    ...
}

当Exec函数重构成Exec变量后,丝毫不影响既有代码中对Exec函数的调用。由于Exec变量是函数变量,所以我们一般将这类变
量也叫做函数。

现在我们可以对Exec函数打桩了,代码如下所示:

stubs := Stub(&Exec, func(cmd string, args ...string) (string, error) {
            return "xxx-vethName100-yyy", nil
})
defer stubs.Reset()

3. 为一个过程打桩
当一个函数没有返回值时,该函数我们一般称为过程。很多时候,我们将资源清理类函数定义为过程。

我们对过程DestroyResource的打桩代码为:

stubs := StubFunc(&DestroyResource)
defer stubs.Reset()

测试案例

结合GoConvey,测试函数中嵌套两级Convey语句,第一级Convey语句对应测试函数,第二级Convey语句对应测试用例。在第二级的每个Convey函数中都会产生一个stubs对象,彼此独立,互不影响。

func TestFuncDemo(t *testing.T) {
    Convey("TestFuncDemo", t, func() {
        Convey("for succ", func() {
            stubs := Stub(&num, 150)
            defer stubs.Reset()
            stubs.StubFunc(&Exec,"xxx-vethName100-yyy", nil)
            var liLei = `{"name":"LiLei", "age":"21"}`
            stubs.StubFunc(&adapter.Marshal, []byte(liLei), nil)
            stubs.StubFunc(&DestroyResource)
            //several So assert
        })

        Convey("for fail when num is too small", func() {
            stubs := Stub(&num, 50)
            defer stubs.Reset()
            //several So assert
        })

        Convey("for fail when Exec error", func() {
            stubs := Stub(&num, 150)
            defer stubs.Reset()
            stubs.StubFunc(&Exec, "", ErrAny)
            //several So assert
        })

        Convey("for fail when Marshal error", func() {
            stubs := Stub(&num, 150)
            defer stubs.Reset()
            stubs.StubFunc(&Exec,"xxx-vethName100-yyy", nil)
            stubs.StubFunc(&adapter.Marshal, nil, ErrAny)
            //several So assert
        })

    })
}

Gostub不适用的复杂情况

  1. 被测函数中多次调用了数据库读操作函数接口 ReadDb,并且数据库为key-value型。被测函数先是 ReadDb 了一个父目录的值,然后在 for 循环中读了若干个子目录的值。在多个测试用例中都有将ReadDb打桩为在多次调用中呈现不同行为的需求,即父目录的值不同于子目录的值,并且子目录的值也互不相等

  2. 被测函数中有一个循环,用于一个批量操作,当某一次操作失败,则返回失败,并进行错误处理。假设该操作为Apply,则在异常的测试用例中有将Apply打桩为在多次调用中呈现不同行为的需求,即Apply的前几次调用返回成功但最后一次调用却返回失败

  3. 被测函数中多次调用了同一底层操作函数,比如exec.Command,函数参数既有命令也有命令参数。被测函数先是创建了一个对象,然后查询对象的状态,在对象状态达不到期望时还要删除对象,其中查询对象是一个重要的操作,一般会进行多次重试。在多个测试用例中都有将 exec.Command 打桩为多次调用中呈现不同行为的需求,即创建对象、查询对象状态和删除对象对返回值的期望都不一样

摘自:https://www.jianshu.com/p/70a93a9ed186