Skip to content

单元测试工具 testify 用法

标签
开发/代码/代码片段
开发/语言/Golang
开发/单元测试
开发/语言/Golang/godailylib
字数
5601 字
阅读时间
26 分钟

简述 testify可以说是最流行的(从 GitHub star 数来看)Go 语言测试库了。testify提供了很多方便的函数帮助我们做assert和错误信息输出。使用标准库testing,我们需要自己编写各种条件判断,根据判断结果决定输出对应的信息。


简介

testify可以说是最流行的(从 GitHub star 数来看)Go 语言测试库了。testify 提供了很多方便的函数帮助我们做 assert 和错误信息输出。使用标准库 testing,我们需要自己编写各种条件判断,根据判断结果决定输出对应的信息。

testify 核心有三部分内容:

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

准备工作

本文代码使用 Go Modules(即 go mod)。

创建目录并初始化:

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

安装 testify 库:

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

assert

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

go
func TestGot(t *testing.T) {
	// 当获取值 got 不等于期望值 expected 时
	if got != expected {
	  // 使用 testing.T.Errorf 打印错误信息并标记行号
	  t.Errorf("Xxx failed expect:%d got:%d", got, expected)
	}
}

简化为一行断言代码:

go
func TestGot(t *testing.T) {
	assert.Equal(t, got, expected, "they should be equal")
}

由于 testify 包也给 assertrequire 提供了实例化的函数 New(t *testing.T) ,上面的代码中,Equal 函数的第一个 t 也可以被实例化的 assertrequire 省略(这在多行测试或者多个断言时会变得十分有效),比如:

go
func TestGot(t *testing.T) {
	assert := assert.New(t)
	assert.Equal(got, expected, "they should be equal")
}

结构更清晰,更可读。熟悉其他语言测试框架的开发者对 assert 的相关用法应该不会陌生。此外,assert 中的函数会自动生成比较清晰的错误描述信息:

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

使用 testify 编写测试代码与 testing (Golang 内置的单元测试套件包)要求的单元测试一样,测试文件结尾为 _test.go,测试函数命名要求前缀为 Test(比如 TestXxxx)。 使用 go test 命令运行测试:

shell
$ go test
--- FAIL: TestEqual (0.00s) # <- 该行说明是哪个测试函数出现问题
    assert\main_test.go:12: # <- 该行说明是哪个测试文件和对应的行号出问题
                Error Trace: # <- 错误追踪
                Error:          Not equal: # <- 说明「不等于」
                                expected: 100 # <- 期望 100
                                actual  : 200 # <- 实际 200
                Test:           TestEqual # <- 测试函数名称
FAIL # <- 说明失败了
exit status 1 # <- 单元测试以非 0 值退出,单元测试程序返回 1
FAIL    github.com/me/main   0.107s # <- 说明该包测试失败

我们看到信息更易读。

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

go
// Equal 函数定义,第一个为 t,类型和 *testing.T 相同,第二个为期望值,第三个为实际值,第四个为出错时打印的值
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{})
// Equal 函数定义,第一个为 t,类型和 *testing.T 相同,第二个为期望值,第三个为实际值,第四个为出错时打印的格式字符串,第五个为参数列表,即需要格式化的参数
func Equalf(t TestingT, expected, actual interface{}, msg string, args ...interface{})

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

go
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 的版本即可。

Assertions 实例化对象

观察到上面的断言都是以 TestingT 为第一个参数,需要大量使用时比较麻烦。testify 提供了一种方便的方式。先以 *testing.T 为参数调用 assert.New() 函数创建一个 *Assertions 对象,Assertions 定义了前面所有的断言方法,只是不需要再传入 TestingT 参数了。

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

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

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

True

函数类型:

go
func (a *Assertions) True(value bool, msgAndArgs ...interface{})

True 函数断言参数 value 为布尔值的 true

False

函数类型:

go
func (a *Assertions) False(value bool, msgAndArgs ...interface{})

False 函数断言参数 value 为布尔值的 false

Zero

函数类型:

go
func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{})

Zero 函数断言参数 i 为其所属类型的零值。不同的类型零值不同,参考:

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

NotZero

函数类型:

go
func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{})

NotZero 函数断言参数 i 为其所属类型的零值。不同的类型零值不同,参考:

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

Condition

函数类型:

go
func (a *Assertions) Condition(comp Comparison, msgAndArgs ...interface{}) bool {

Condition 函数允许传入一个自定义对比和断言复杂逻辑的函数作为参数 comp 。通常用在非常复杂的断言逻辑上。

Contains

函数类型:

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

Contains 函数断言 s 参数包含 contains 参数的值。其中 s 可以是字符串,数组/切片,map。相应地,contains 可以为子字符串,数组/切片元素,map 的键。

NotContains

函数类型:

go
func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{})

NotContains 函数断言 s 参数包含 contains 参数的值。其中 s 可以是字符串,数组/切片,map。相应地,contains 可以为子字符串,数组/切片元素,map 的键。

DirExists

函数类型:

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

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

NoDirExists

函数类型:

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

NoDirExists 断言路径 path 是一个目录,或者 path 存在,断言失败。

ElementsMatch

函数类型:

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

ElementsMatch 断言 listAlistB 两个切片/数组包含相同的元素,忽略元素出现的顺序。listA/listB 必须是数组或切片。如果有重复元素,重复元素出现的次数也必须相等。

Empty

函数类型:

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

Empty断言 object 是空,根据 object 中存储的实际类型,空的含义不同:

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

NotEmpty

函数类型:

go
func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{})

NotEmpty 断言 object 为空,根据 object 中存储的实际类型,非空的含义不同:

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

Equal

函数类型:

go
func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{})

Equal 断言 expectedactual 的值相等。

NotEqual

函数类型:

go
func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {

NotEqual 断言 expectedactual 的值相等

EqualError

函数类型:

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

EqualError 断言 theError.Error() 的返回值与 errString 相等。用于直接对比两个错误的字符串值是否相同。

EqualValues

函数类型:

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

EqualValues 断言 expectedactual 相等,或者可以转换为相同的类型,并且相等。这个条件比 Equal 更宽,Equal() 返回 trueEqualValues() 肯定也返回 true,反之则不然。实现的核心是下面两个函数,使用了reflect.DeapEqual()

go
// assert.Equal 调用的函数
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
  }
  // 对比 nil
  if exp == nil || act == nil {
    return exp == nil && act == nil
  }
  // 通过字节码对比
  return bytes.Equal(exp, act)
}

// assert.EqualValues
func ObjectsAreEqualValues(expected, actual interface{}) bool {
  // 首先调用上面定义的 ObjectsAreEqual 函数
  if ObjectsAreEqual(expected, actual) {
    return true
  }

  // 通过反射包在运行时获取 actual 参数的类型
  actualType := reflect.TypeOf(actual)
  if actualType == nil {
    return false
  }
  // 通过反射包在运行时获取 expected 参数的类型
  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(因为 Equal 不考虑类型是否相互兼容),而 EqualValues() 会返回 true:

go
type MyInt int

func TestEqual(t \*testing.T) {
  var a = 100
  var b MyInt = 100
  assert.Equal(t, a, b, "") // 测试提示失败 Fail
  assert.EqualValues(t, a, b, "") // 测试提示成功 Pass
}

EqualValues

函数类型:

go
func (a *Assertions) NotEqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{})

EqualValues 断言 expectedactual 相等,也可以转换为相同的类型

Error

函数类型:

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

Error 断言 err 不为 nil

NoError

函数类型:

go
func (a *Assertions) NoError(err error, msgAndArgs ...interface{})

NoError 断言 errnil

ErrorAs

函数类型:

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

ErrorAs 断言 err 表示的 error 链中至少有一个和 target 匹配。这个函数是对标准库中 errors.As 的包装。

ErrorIs

函数类型:

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

ErrorIs 断言 err 的 error 链中有 target

NotErrorIs

函数类型:

go
func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{})

NotErrorIs 断言 err 的 error 链中target

Eventually

函数类型:

go
func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool

Eventually 函数允许执行一个回调函数作为参数 condition ,并且在期望的时间参数 waitFor 内完成,该函数将会在参数 tick 时长的周期内不断检查回调函数是否结束。

Exactly

函数类型:

go
func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool

Exactly 函数和比 EqualEqualValues 函数都要严格,要求期望值 expected 和实际值 actual 的类型和值完全相同。

Fail

函数类型:

go
func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) bool

Fail 函数可以在调用的时候打印失败的信息和调用行的行号。接受一个参数 failureMessage 作为失败的信息。

FailNow

函数类型:

go
func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) bool

FailNow 函数可以在调用的时候打印失败的信息和调用行的行号,并且立即使当前进行的单元测试结束。接受一个参数 failureMessage 作为失败的信息。

FileExists

函数类型:

go
func (a *Assertions) FileExists(path string, msgAndArgs ...interface{})

FileExists 断言 path 是一个文件,并且存在于 path

NoFileExists

函数类型:

go
func (a *Assertions) NoFileExists(path string, msgAndArgs ...interface{})

NoFileExists 断言 path 是一个文件,或者不存在于 path

Greater

函数类型:

go
func (a *Assertions) Greater(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool

Greater 函数可以对比大小,断言参数 e1 大于参数 e2

GreaterOrEqual

函数类型:

go
func (a *Assertions) GreaterOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool {

GreaterOrEqual 函数可以对比大小,断言参数 e1 大于或等于参数 e2

Len

函数类型:

go
func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) bool

Len 函数可以对比和断言参数 object 的长度是否为参数 length 的大小,object 必须是数组、切片、map 的其中一种

Less

函数类型:

go
func (a *Assertions) Less(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool

Less 函数可以对比大小,断言参数 e1 小于参数 e2

LessOrEqual

函数类型:

go
func (a *Assertions) LessOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool

LessOrEqual 函数可以对比大小,断言参数 e1 小于或等于参数 e2

Nil

函数类型:

go
func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) bool

Nil 函数断言参数 objectNil

NotNil

函数类型:

go
func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{})

NotNil 函数断言参数 object 不为 Nil

Panics

函数类型:

go
func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool

Panics 函数期望回调函数 f 发生恐慌 panic。

NoPanics

函数类型:

go
func (a *Assertions) NotPanics(f assert.PanicTestFunc, msgAndArgs ...interface{})

NoPanics 函数期望回调函数 f 不会发生恐慌 panic。

PanicsWithValue

函数类型:

go
func (a *Assertions) PanicsWithValue(expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool

PanicsWithValue 函数期望回调函数 f 发生恐慌 panic,并且恐慌返回一个值,值为参数 expected

逆断言

上一章节提到的断言中,有部分的断言函数有对应的逆断言,列表:

require

require 提供了和 assert 同样的接口,但是遇到错误时,require 直接终止测试,而 assert 会返回布尔值 false。 在某些单元测试中需要对指针、数组、map 等复合类型进行断言时,这十分有用(比如如果期望之后会访问切片的1 的索引位置的值,不做 require 断言的话,可能会导致单元测试 panic 恐慌)。

函数可以参考上面 assert 章节的的定义。

mock

testify 提供了对 Mock 的简单支持。Mock 简单来说就是构造一个仿对象,仿对象提供和原对象一样的接口,在测试中用仿对象来替换原对象。这样我们可以在原对象很难构造,特别是涉及外部资源(数据库,访问网络等)。例如,我们现在要编写一个从一个站点拉取用户列表信息的程序,拉取完成之后程序显示和分析。如果每次都去访问网络会带来极大的不确定性,甚至每次返回不同的列表,这就给测试带来了极大的困难。我们可以使用 Mock 技术。

go
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接口:

go
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 对象做断言。

运行:

shell
$ 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 次:

go
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 对象:

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

运行:

shell
$ 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() 函数中<=应该是<导致多调用了一次,修改过来继续运行:

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

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

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

suite

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

go
type SetupAllSuite interface {
  SetupSuite()
}

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

go
type TearDownAllSuite interface {
  TearDownSuite()
}

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

go
type SetupTestSuite interface {
  SetupTest()
}

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

go
type TearDownTestSuite interface {
  TearDownTest()
}

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

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

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

go
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()运行测试套件:

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

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

shell
$ 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 服务器:

go
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接口,但是它只是记录写入的状态码和响应内容,不会发送响应给客户端。这样我们可以将该类型的对象传给处理器函数。然后构造服务器,传入该对象来驱动请求处理流程,最后测试该对象中记录的信息是否正确:

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

运行:

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

很简单,没有问题。

但是我们发现一个问题,上面的很多代码有重复,recorder/mux 等对象的创建,处理器函数的注册。使用 suite 我们可以集中创建,省略这些重复的代码:

go
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驱动测试:

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

总结

testify 扩展了 Golang 自带的 testing 标准库,断言库 assertrequire,测试替身 mock 和测试套件 suite,让我们编写测试代码更容易!

贡献者

页面历史

撰写

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。