现在,你已经基本了解了Go单元测试的函数写法,以及是如何执⾏的。⽽且你也知道⼀个单元测试的函数的函数签名如下所示:

func TestXxx(t *testing.T) {
...
}

注意,这个函数是没有返回类型的,⽽且必须有且只有⼀个testing.T 的参数。当单元测试执⾏的时候,它会传⼊ *testing.T 的参数,你可以在函数体中使⽤这个参数,标记测试是否成功或者失败,输出⼀些⽇志,或者执⾏⼦测试。

*testing.T 和⽤来做benchmark的 *testing.B 以及做模糊测试的 *testing.F ⼀样,都实现了 testing.TB 接⼝。这个接⼝的名字也很有趣,应该先前是 TB 共同的接⼝,所以叫 TB ,不过后来Go 1.18加⼊了Fuzzing测试,所以接⼝名是不是应该是 TBF 了?不过这不重要,⼀般我们很少直接使⽤ *testing.TB

这⼀章我们只介绍 *testing.T ,也就顺便介绍了 TB 的⽅法。 TBF 特有的⽅法我们遇到的时候专⻔介绍。

我将 *testing.T 的⽅法按照功能分成⼏类进⾏介绍。

标记测试是否失败 (Fail/FailNow/Failed/Fatal/Fatalf)

  • Fail: 标记当前测试失败,但是后续的代码还是继续执⾏,不会中断
  • FailNow: 标记当前测试失败,并且调⽤ runtime.Goexit 停⽌本goroutine的执⾏。
  • 因为只是调⽤的 runtime.Goexit ,⽽不是 os.Exit ,所以它只是停⽌了本测试,后续的测试还是会被执⾏。
  • Fatal: 先输出⼀个⽇志(Log),再标记当前测试失败停⽌执⾏。等价于Log + FailNow
  • Fatalf: 等价于 Logf + FailNow
  • Failed: 返回当前函数是否已经被标记为failed。

例⼦:

package s2
import (
    "testing"
    "time"
)
func TestFail(t *testing.T) {
    t.Fail()
    t.Log("after Fail")
    t.FailNow()
    t.Log("after FailNow")
}
func TestFatal(t *testing.T) {
    t.Fatal("fataled")
    t.Log("after Fatal")
}
func TestFatalf(t *testing.T) {
    t.Fatalf("there is: %v", "Fatalf")
    t.Log("after Fatalf")
}
func TestFailNowInAnotherGoroutine(t *testing.T) {
    go func() {
        t.FailNow()
        t.Log("after FailNow in another goroutine")
    }()
    time.Sleep(time.Second)
    t.Log("after one second")
}

输出⽇志 (Log/Logf/Error/Errorf)

这⼏个⽅法⽤来输出⽇志信息。

  • Log: 类似 Println ,输出到error log中。对于单元测试,只有测试失败或者使⽤ -test.v 参数时,才会真正输出⽇志。但是对于benchmark,总是会输出。
  • Logf: 类似 Printf ,可以输出格式化的⽇志。
  • Error: 等价于 Log + Fail
  • Errorf: 等价于 Logf + Fail

例⼦:

package s2

import (
    "testing"
    "time"
)
func TestLog(t *testing.T) {
    t.Log("it is a log")
    t.Logf("it is a log at %v", time.Now().Format(time.RFC1123))
    t.Error("it is an error")
    t.Errorf("it is an error at %v", time.Now().Format(time.RFC1123))
}

跳过(Skip/SkipNow/Skipped/equivalent)

  • SkipNow: 跳过本测试,并调⽤ runtime.Goexit 停⽌执⾏本测试。
  • Skip: 等价于 Log + SkipNow
  • Skipf: 等价于 Logf + SkipNow
  • Skipped: 返回当前测试是否被跳过。

例子:

package s2
import (
    "runtime"
    "testing"
)
func TestSkip(t *testing.T) {
    if runtime.GOOS == "darwin" {
        t.Skip("skip MacOS")
    }
    if testing.Short() {
        t.Skip("skip because of short")
    }
    t.Log("there a non-skipped log")
}

那么什么时候使⽤Skip⽅法呢?

⽐如你在本机编写单元测试函数,这些函数依赖⼀些基础的服务,或者只能在Linux环境下运维⽽你的机器是Mac,这个时候你可能不想执⾏这些单元测试,⽽是在依赖可⽤或者Linux环境下才运⾏。你可以编写好这些单元测试,指定在某种环境下直接跳过,⽽不是报错。

还⽐如有些单元测试需要耗费⽐较⻓的时间。当传⼊ -short 参数时,我们就跳过这些耗时⻓的测试。

Helper

有时候,我们在编写单元测试代码时,可能会使⽤⼀些辅助的函数,这些函数我们称之为 helper函数。当输出错误信息的时候,会显示辅助函数的信息,这些信息实际是不必要的,所以我们可以在这些函数的开始位置调⽤ Helper⽅法,将此函数标记为辅助函数,这样⽇志中就不会显示这些辅助函数的信息了。

⽐如下⾯这个来⾃于官⽅的例⼦:

package s2
import "testing"
func notHelper(t *testing.T, msg string) {
    t.Error(msg)
}
func helper(t *testing.T, msg string) {
    t.Helper()
    t.Error(msg)
}
func notHelperCallingHelper(t *testing.T, msg string) {
    helper(t, msg)
}
func helperCallingHelper(t *testing.T, msg string) {
    t.Helper()
    helper(t, msg)
}
func TestHelper(t *testing.T) {
    notHelper(t, "0")
    helper(t, "1")
    notHelperCallingHelper(t, "2")
    helperCallingHelper(t, "3")
    fn := func(msg string) {
        t.Helper()
        t.Error(msg)
    }
    fn("4")
    t.Helper()
    t.Error("5")
    t.Run("sub", func(t *testing.T) {
        helper(t, "6")
        notHelperCallingHelper(t, "7")
        t.Helper()
        t.Error("8")
    })
}

Parallel

当⼀个单元测试的函数标记为可以并发执⾏的时候,它可以并且只能和其它标记为Parallel的函数并发执⾏。

当使⽤ -test.count-test.cpu 参数时,同⼀个单元测试的多个实例之间不会并发执⾏。

package s2

import (
    "net/http"
    "testing"
)
func TestParallel(t *testing.T) {
    var urls = map[string]string{"baidu": "http://baidu.com", "bing":
    "http://bing.com", "google": "http://google.com"}
    for k, v := range urls {
        v := v
        ok := t.Run(k, func(t *testing.T) {
            t.Parallel()
            t.Logf("start to get %s", v)
            resp, err := http.Get(v)
            if err != nil {
                t.Fatalf("failed to get %s: %v", v, err)
            }
            resp.Body.Close()
        })
        t.Logf("run: %t", ok)
    }
}

Cleanup

有时候我们写单元测试的时候,需要做⼀些准备的动作,⽐如建⽴与数据库的连接,打开⼀个⽂件、创建⼀个临时⽂件等等,执⾏案单元测试我们还想执⾏⼀些清理的动作,⽐如关闭与数据库的连接,关闭⽂件,删除临时⽂件等等,这个时候我们就可以使⽤Cleanup⽅法了:

package s2
import (
    "io/ioutil"
    "os"
    "path/filepath"
    "testing"
)
func TestWrite(t *testing.T) {
    tempDir, err := ioutil.TempDir(".", "temp")
    if err != nil {
        t.Errorf("create tempDir: %v", err)
    }
    t.Cleanup(func() { os.RemoveAll(tempDir) })
    err = ioutil.WriteFile(filepath.Join(tempDir, "test.log"), []byte("hello test"), 0644)
    if err != nil {
        t.Errorf("want writing success but got %v", err)
    }
}

TestMain

Go还提供了TestMain的功能。 TestMain并不是⽤来测试main函数,⽽是对同⼀个package下的所有测试有统⼀的控制。

⽐如所有的测试都数据数据库的连接,那么可以在 m.Run 之前把数据库连接准备好,在执⾏完测试之后把数据库关闭。

你可以看到它和 Cleanup 不太⼀样, Cleanup 针对的是⼀个单元测试函数,⽽ TestMain 针对的是同⼀个package所有的测试。

import (
    "log"
    "os"
    "testing"
)
func TestMain(m *testing.M) {
    log.Println("do stuff BEFORE the tests!")
    exitVal := m.Run()
    log.Println("do stuff AFTER the tests!")

    os.Exit(exitVal)
}
作者:admin  创建时间:2024-10-22 01:54
最后编辑:admin  更新时间:2024-10-22 02:06