乐趣区

关于golang:Go-每日一库之-testify

简介

testify能够说是最风行的(从 GitHub star 数来看)Go 语言测试库了。testify提供了很多不便的函数帮忙咱们做 assert 和错误信息输入。应用规范库testing,咱们须要本人编写各种条件判断,依据判断后果决定输入对应的信息。

testify外围有三局部内容:

  • assert:断言;
  • mock:测试替身;
  • suite:测试套件。

筹备工作

本文代码应用 Go Modules。

创立目录并初始化:

$ mkdir -p testify && cd testify
$ go mod init github.com/darjun/go-daily-lib/testify

装置 testify 库:

$ go get -u github.com/stretchr/testify

assert

assert子库提供了便捷的 断言 函数,能够大大简化测试代码的编写。总的来说,它将之前须要 判断 + 信息输入的模式

if got != expected {t.Errorf("Xxx failed expect:%d got:%d", got, expected)
}

简化为一行 断言 代码:

assert.Equal(t, got, expected, "they should be equal")

构造更清晰,更可读。相熟其余语言测试框架的开发者对 assert 的相干用法应该不会生疏。此外,assert中的函数会主动生成比拟清晰的谬误形容信息:

func TestEqual(t *testing.T) {
  var a = 100
  var b = 200
  assert.Equal(t, a, b, "")
}

应用 testify 编写测试代码与 testing 一样,测试文件为 _test.go,测试函数为TestXxx。应用go test 命令运行测试:

$ go test
--- FAIL: TestEqual (0.00s)
    assert_test.go:12:
                Error Trace:
                Error:          Not equal:
                                expected: 100
                                actual  : 200
                Test:           TestEqual
FAIL
exit status 1
FAIL    github.com/darjun/go-daily-lib/testify/assert   0.107s

咱们看到信息更易读。

testify提供的 assert 类函数泛滥,每种函数都有两个版本,一个版本是函数名不带 f 的,一个版本是带 f 的,区别就在于带 f 的函数,咱们须要指定至多两个参数,一个格式化字符串format,若干个参数args

func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{})
func Equalf(t TestingT, expected, actual interface{}, msg string, args ...interface{})

实际上,在 Equalf() 函数外部调用了Equal()

func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {if h, ok := t.(tHelper); ok {h.Helper()
  }
  return Equal(t, expected, actual, append([]interface{}{msg}, args...)...)
}

所以,咱们只须要关注不带 f 的版本即可。

Contains

函数类型:

func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool

Contains断言 s 蕴含 contains。其中s 能够是字符串,数组 / 切片,map。相应地,contains为子串,数组 / 切片元素,map 的键。

DirExists

函数类型:

func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool

DirExists断言门路 path 是一个目录,如果 path 不存在或者是一个文件,断言失败。

ElementsMatch

函数类型:

func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) bool

ElementsMatch断言 listAlistB蕴含雷同的元素,疏忽元素呈现的程序。listA/listB必须是数组或切片。如果有反复元素,反复元素呈现的次数也必须相等。

Empty

函数类型:

func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool

Empty断言 object 是空,依据 object 中存储的理论类型,空的含意不同:

  • 指针:nil
  • 整数:0;
  • 浮点数:0.0;
  • 字符串:空串""
  • 布尔:false;
  • 切片或 channel:长度为 0。

EqualError

函数类型:

func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool

EqualError断言 theError.Error() 的返回值与 errString 相等。

EqualValues

函数类型:

func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool

EqualValues断言 expectedactual相等,或者能够转换为雷同的类型,并且相等。这个条件比 Equal 更宽,Equal()返回 trueEqualValues()必定也返回true,反之则不然。实现的外围是上面两个函数,应用了reflect.DeapEqual()

func ObjectsAreEqual(expected, actual interface{}) bool {
  if expected == nil || actual == nil {return expected == actual}

  exp, ok := expected.([]byte)
  if !ok {return reflect.DeepEqual(expected, actual)
  }

  act, ok := actual.([]byte)
  if !ok {return false}
  if exp == nil || act == nil {return exp == nil && act == nil}
  return bytes.Equal(exp, act)
}

func ObjectsAreEqualValues(expected, actual interface{}) bool {
    // 如果 `ObjectsAreEqual` 返回 true,间接返回
  if ObjectsAreEqual(expected, actual) {return true}

  actualType := reflect.TypeOf(actual)
  if actualType == nil {return false}
  expectedValue := reflect.ValueOf(expected)
  if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
    // 尝试类型转换
    return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual)
  }

  return false
}

例如我基于 int 定义了一个新类型 MyInt,它们的值都是 100,Equal() 调用将返回 false,EqualValues()会返回 true:

type MyInt int

func TestEqual(t *testing.T) {
  var a = 100
  var b MyInt = 100
  assert.Equal(t, a, b, "")
  assert.EqualValues(t, a, b, "")
}

Error

函数类型:

func Error(t TestingT, err error, msgAndArgs ...interface{}) bool

Error断言 err 不为nil

ErrorAs

函数类型:

func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool

ErrorAs断言 err 示意的 error 链中至多有一个和 target 匹配。这个函数是对规范库中 errors.As 的包装。

ErrorIs

函数类型:

func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool

ErrorIs断言 err 的 error 链中有target

逆断言

下面的断言都是它们的逆断言,例如 NotEqual/NotEqualValues 等。

Assertions 对象

察看到下面的断言都是以 TestingT 为第一个参数,须要大量应用时比拟麻烦。testify提供了一种不便的形式。先以 *testing.T 创立一个 *Assertions 对象,Assertions定义了后面所有的断言办法,只是不须要再传入 TestingT 参数了。

func TestEqual(t *testing.T) {assertions := assert.New(t)
  assertion.Equal(a, b, "")
  // ...
}

顺带提一句 TestingT 是一个接口,对 *testing.T 做了一个简略的包装:

type TestingT interface{Errorf(format string, args ...interface{})
}

require

require提供了和 assert 同样的接口,然而遇到谬误时,require间接终止测试,而 assert 返回false

mock

testify提供了对 Mock 的简略反对。Mock 简略来说就是结构一个 仿对象,仿对象提供和原对象一样的接口,在测试中用仿对象来替换原对象。这样咱们能够在原对象很难结构,特地是波及内部资源(数据库,拜访网络等)。例如,咱们当初要编写一个从一个站点拉取用户列表信息的程序,拉取实现之后程序显示和剖析。如果每次都去拜访网络会带来极大的不确定性,甚至每次返回不同的列表,这就给测试带来了极大的艰难。咱们能够应用 Mock 技术。

package main

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "net/http"
)

type User struct {
  Name string
  Age  int
}

type ICrawler interface {GetUserList() ([]*User, error)
}

type MyCrawler struct {url string}

func (c *MyCrawler) GetUserList() ([]*User, error) {resp, err := http.Get(c.url)
  if err != nil {return nil, err}

  defer resp.Body.Close()
  data, err := ioutil.ReadAll(resp.Body)
  if err != nil {return nil, err}

  var userList []*User
  err = json.Unmarshal(data, &userList)
  if err != nil {return nil, err}

  return userList, nil
}

func GetAndPrintUsers(crawler ICrawler) {users, err := crawler.GetUserList()
  if err != nil {return}

  for _, u := range users {fmt.Println(u)
  }
}

Crawler.GetUserList()办法实现爬取和解析操作,返回用户列表。为了不便 Mock,GetAndPrintUsers()函数承受一个 ICrawler 接口。当初来定义咱们的 Mock 对象,实现 ICrawler 接口:

package main

import (
  "github.com/stretchr/testify/mock"
  "testing"
)

type MockCrawler struct {mock.Mock}

func (m *MockCrawler) GetUserList() ([]*User, error) {args := m.Called()
  return args.Get(0).([]*User), args.Error(1)
}

var (MockUsers []*User
)

func init() {MockUsers = append(MockUsers, &User{"dj", 18})
  MockUsers = append(MockUsers, &User{"zhangsan", 20})
}

func TestGetUserList(t *testing.T) {crawler := new(MockCrawler)
  crawler.On("GetUserList").Return(MockUsers, nil)

  GetAndPrintUsers(crawler)

  crawler.AssertExpectations(t)
}

实现 GetUserList() 办法时,须要调用 Mock.Called() 办法,传入参数(示例中无参数)。Called()会返回一个 mock.Arguments 对象,该对象中保留着返回的值。它提供了对根本类型和 error 的获取办法Int()/String()/Bool()/Error(),和通用的获取办法Get(),通用办法返回interface{},须要类型断言为具体类型,它们都承受一个示意索引的参数。

crawler.On("GetUserList").Return(MockUsers, nil)是 Mock 施展魔法的中央,这里批示调用 GetUserList() 办法的返回值别离为 MockUsersnil,返回值在下面的 GetUserList() 办法中被 Arguments.Get(0)Arguments.Error(1)获取。

最初 crawler.AssertExpectations(t) 对 Mock 对象做断言。

运行:

$ go test
&{dj 18}
&{zhangsan 20}
PASS
ok      github.com/darjun/testify       0.258s

GetAndPrintUsers()函数性能失常执行,并且咱们通过 Mock 提供的用户列表也能正确获取。

应用 Mock,咱们能够准确断言某办法以特定参数的调用次数,Times(n int),它有两个便捷函数 Once()/Twice()。上面咱们要求函数Hello(n int) 要以参数 1 调用 1 次,参数 2 调用两次,参数 3 调用 3 次:

type IExample interface {Hello(n int) int
}

type Example struct {
}

func (e *Example) Hello(n int) int {fmt.Printf("Hello with %d\n", n)
  return n
}

func ExampleFunc(e IExample) {
  for n := 1; n <= 3; n++ {
    for i := 0; i <= n; i++ {e.Hello(n)
    }
  }
}

编写 Mock 对象:

type MockExample struct {mock.Mock}

func (e *MockExample) Hello(n int) int {args := e.Mock.Called(n)
  return args.Int(0)
}

func TestExample(t *testing.T) {e := new(MockExample)

  e.On("Hello", 1).Return(1).Times(1)
  e.On("Hello", 2).Return(2).Times(2)
  e.On("Hello", 3).Return(3).Times(3)

  ExampleFunc(e)

  e.AssertExpectations(t)
}

运行:

$ go test
--- FAIL: TestExample (0.00s)
panic:
assert: mock: The method has been called over 1 times.
        Either do one more Mock.On("Hello").Return(...), or remove extra call.
        This call was unexpected:
                Hello(int)
                0: 1
        at: [equal_test.go:13 main.go:22] [recovered]

原来 ExampleFunc() 函数中 <= 应该是 < 导致多调用了一次,批改过去持续运行:

$ go test
PASS
ok      github.com/darjun/testify       0.236s

咱们还能够设置以指定参数调用会导致 panic,测试程序的健壮性:

e.On("Hello", 100).Panic("out of range")

suite

testify提供了测试套件的性能(TestSuite),testify测试套件只是一个构造体,内嵌一个匿名的 suite.Suite 构造。测试套件中能够蕴含多个测试,它们能够共享状态,还能够定义钩子办法执行初始化和清理操作。钩子都是通过接口来定义的,实现了这些接口的测试套件构造在运行到指定节点时会调用对应的办法。

type SetupAllSuite interface {SetupSuite()
}

如果定义了 SetupSuite() 办法(即实现了 SetupAllSuite 接口),在套件中所有测试开始运行前调用这个办法。对应的是TearDownAllSuite

type TearDownAllSuite interface {TearDownSuite()
}

如果定义了 TearDonwSuite() 办法(即实现了 TearDownSuite 接口),在套件中所有测试运行实现后调用这个办法。

type SetupTestSuite interface {SetupTest()
}

如果定义了 SetupTest() 办法(即实现了 SetupTestSuite 接口),在套件中每个测试执行前都会调用这个办法。对应的是TearDownTestSuite

type TearDownTestSuite interface {TearDownTest()
}

如果定义了 TearDownTest() 办法(即实现了 TearDownTest 接口),在套件中每个测试执行后都会调用这个办法。

还有一对接口BeforeTest/AfterTest,它们别离在每个测试运行前 / 后调用,承受套件名和测试名作为参数。

咱们来编写一个测试套件构造作为演示:

type MyTestSuit struct {
  suite.Suite
  testCount uint32
}

func (s *MyTestSuit) SetupSuite() {fmt.Println("SetupSuite")
}

func (s *MyTestSuit) TearDownSuite() {fmt.Println("TearDownSuite")
}

func (s *MyTestSuit) SetupTest() {fmt.Printf("SetupTest test count:%d\n", s.testCount)
}

func (s *MyTestSuit) TearDownTest() {
  s.testCount++
  fmt.Printf("TearDownTest test count:%d\n", s.testCount)
}

func (s *MyTestSuit) BeforeTest(suiteName, testName string) {fmt.Printf("BeforeTest suite:%s test:%s\n", suiteName, testName)
}

func (s *MyTestSuit) AfterTest(suiteName, testName string) {fmt.Printf("AfterTest suite:%s test:%s\n", suiteName, testName)
}

func (s *MyTestSuit) TestExample() {fmt.Println("TestExample")
}

这里只是简略在各个钩子函数中打印信息,统计执行实现的测试数量。因为要借助 go test 运行,所以须要编写一个 TestXxx 函数,在该函数中调用 suite.Run() 运行测试套件:

func TestExample(t *testing.T) {suite.Run(t, new(MyTestSuit))
}

suite.Run(t, new(MyTestSuit))会将运行 MyTestSuit 中所有名为 TestXxx 的办法。运行:

$ go test
SetupSuite
SetupTest test count:0
BeforeTest suite:MyTestSuit test:TestExample
TestExample
AfterTest suite:MyTestSuit test:TestExample
TearDownTest test count:1
TearDownSuite
PASS
ok      github.com/darjun/testify       0.375s

测试 HTTP 服务器

Go 规范库提供了一个 httptest 用于测试 HTTP 服务器。当初编写一个简略的 HTTP 服务器:

func index(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello World")
}

func greeting(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "welcome, %s", r.URL.Query().Get("name"))
}

func main() {mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/greeting", greeting)

  server := &http.Server{
    Addr:    ":8080",
    Handler: mux,
  }

  if err := server.ListenAndServe(); err != nil {log.Fatal(err)
  }
}

很简略。httptest提供了一个 ResponseRecorder 类型,它实现了 http.ResponseWriter 接口,然而它只是记录写入的状态码和响应内容,不会发送响应给客户端。这样咱们能够将该类型的对象传给处理器函数。而后结构服务器,传入该对象来驱动申请解决流程,最初测试该对象中记录的信息是否正确:

func TestIndex(t *testing.T) {recorder := httptest.NewRecorder()
  request, _ := http.NewRequest("GET", "/", nil)
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/greeting", greeting)

  mux.ServeHTTP(recorder, request)

  assert.Equal(t, recorder.Code, 200, "get index error")
  assert.Contains(t, recorder.Body.String(), "Hello World", "body error")
}

func TestGreeting(t *testing.T) {recorder := httptest.NewRecorder()
  request, _ := http.NewRequest("GET", "/greeting", nil)
  request.URL.RawQuery = "name=dj"
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/greeting", greeting)

  mux.ServeHTTP(recorder, request)

  assert.Equal(t, recorder.Code, 200, "greeting error")
  assert.Contains(t, recorder.Body.String(), "welcome, dj", "body error")
}

运行:

$ go test
PASS
ok      github.com/darjun/go-daily-lib/testify/httptest 0.093s

很简略,没有问题。

然而咱们发现一个问题,下面的很多代码有反复,recorder/mux等对象的创立,处理器函数的注册。应用 suite 咱们能够集中创立,省略这些反复的代码:

type MySuite struct {
  suite.Suite
  recorder *httptest.ResponseRecorder
  mux      *http.ServeMux
}

func (s *MySuite) SetupSuite() {s.recorder = httptest.NewRecorder()
  s.mux = http.NewServeMux()
  s.mux.HandleFunc("/", index)
  s.mux.HandleFunc("/greeting", greeting)
}

func (s *MySuite) TestIndex() {request, _ := http.NewRequest("GET", "/", nil)
  s.mux.ServeHTTP(s.recorder, request)

  s.Assert().Equal(s.recorder.Code, 200, "get index error")
  s.Assert().Contains(s.recorder.Body.String(), "Hello World", "body error")
}

func (s *MySuite) TestGreeting() {request, _ := http.NewRequest("GET", "/greeting", nil)
  request.URL.RawQuery = "name=dj"

  s.mux.ServeHTTP(s.recorder, request)

  s.Assert().Equal(s.recorder.Code, 200, "greeting error")
  s.Assert().Contains(s.recorder.Body.String(), "welcome, dj", "body error")
}

最初编写一个 TestXxx 驱动测试:

func TestHTTP(t *testing.T) {suite.Run(t, new(MySuite))
}

总结

testify扩大了 testing 规范库,断言库 assert,测试替身mock 和测试套件suite,让咱们编写测试代码更容易!

大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. testify GitHub:github.com/stretchr/testify
  2. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

我的博客:https://darjun.github.io

欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~

退出移动版