摒弃世俗浮躁
追求技术精湛

Golang笔记(五):单元测试

单元测试的分类

  1. 单元测试 —— 顾名思义,测试的最小单元,测试底层功能函数,例如你写一个数据库连接的类,类里的每一个方法都可用单元测试来保证其可用性;
  2. 集成测试 —— 也称为功能测试,在 Web 开发中测试整个 Web 请求或 HTTP API 请求,会模拟 HTTP 请求到自己提供的服务器上,创建或更改数据,然后检查数据库里这些数据是否变更;
  3. 黑盒测试 —— 完全模拟用户测试,把应用连带服务器看成是一个整体,模拟 HTTP 请求,对返回结果进行断言;

单元测试适用于测试底层函数,集成测试 可模拟表单提交、甚至为应用创建独有的内存数据库、可调用到内部编程接口、更改应用的驱动,功能比较齐全,但是比较难理解,代码写起来也比较复杂,这本身就是一个值得深入学习的课题,本课程不会涉及太深。

黑盒测试 与 集成测试 一样,都是站在用户角度测试整个应用提供的功能,黑盒测试 的好处是代码比较简单,因为模拟用户行为,对新手来讲也比较好理解,缺点就是执行效率低,且无法在代码里自动为其创建专属的测试环境。

Tips:一般来讲,测试环境需要与其他环境区分开,一方面是为了保证测试的准确性,另一方面也是避免污染其他环境。

断言(Assertion)

断言是在代码层面上,我们用以判断获取到的结果是否符合预期,以此来判断测试是否通过。

测试工具

这里推荐 stretchr/testify 第三方测试包,安装命令如下:

$ go get github.com/stretchr/testify

写单元测试前必须需要知道的事情

  • 项目的所有测试文件都需归类到 tests 目录下;
  • 文件的后缀名 _test 是一个特殊标识(例如:pages_test.go),会告知 GO 编译器和工具链这是一个测试文件,GO 编译器在编译时会跳过这些文件。而工具 go test 在执行时默认运行当前目录下所有拥有 _test 后缀名的文件。

对某单元进行测试

package tests

import (
	"github.com/stretchr/testify/assert"
	"net/http"
	"testing"
)

func TestHomePage(t *testing.T) {
	baseUrl := "http://localhost:3000"

	// 1. 请求 -> 模拟用户访问浏览器
	resp, err := http.Get(baseUrl)

	// 2. 检测 -> 是否无错误且返回状态 200
	assert.NoError(t, err, "有错误发生,err不为空")
	assert.Equal(t, http.StatusOK, resp.StatusCode, "应返回状态码 200")
}

我们这里对响应的状态码 resp.StatusCode 以及错误进行断言:

	assert.NoError(t, err, "有错误发生,err不为空")
	assert.Equal(t, http.StatusOK, resp.StatusCode, "应返回状态码 200")

使用 assert.NoError() 来断言没有错误发生。第一个 t 为 testing 标准库里的 testing.T 对象,第二个参数为错误对象 err ,第三个参数为出错时显示的信息(选填)。
assert.Equal() 会断言两个值相等,第一个参数同上,第二个参数是期待的状态码,第三个参数是请求返回的状态码,第四个参数为出错时显示的信息(选填)。

可以在 IDE 里面启动单元测试,也可以执行命令开启测试:到 tests 目录下,执行命令:

$ go test

测试成功:

E:\goProject\src\GoBlog\tests>go test
PASS
ok      GoBlog/tests    0.134s

测试失败:

E:\goProject\src\GoBlog\tests>go test
--- FAIL: TestHomePage (2.37s)
    pages_test.go:16:
                Error Trace:    pages_test.go:16
                Error:          Received unexpected error:
                                Get "http://localhost:3000": dial tcp [::1]:3000: connectex: No connection could be
 made because the target machine actively refused it.
                Test:           TestHomePage
                Messages:       有错误发生,err不为空
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x10 pc=0x329c8a]

goroutine 6 [running]:
testing.tRunner.func1.2(0x35f2a0, 0x589890)
        D:/golang/src/testing/testing.go:1143 +0x345
testing.tRunner.func1(0xc000037080)
        D:/golang/src/testing/testing.go:1146 +0x4b6
panic(0x35f2a0, 0x589890)
        D:/golang/src/runtime/panic.go:965 +0x1c7
GoBlog/tests.TestHomePage(0xc000037080)
        E:/goProject/src/GoBlog/tests/pages_test.go:17 +0xca
testing.tRunner(0xc000037080, 0x3c7088)
        D:/golang/src/testing/testing.go:1193 +0xef
created by testing.(*T).Run
        D:/golang/src/testing/testing.go:1238 +0x2b3
exit status 2
FAIL    GoBlog/tests    2.535s

表组测试

Go 语言中对于相同的测试逻辑场景,我们可以使用简介紧凑的 表组测试(Table-Driven Test)来编写测试,以此来降低测试代码的重复率,标准库中的测试也广泛使用该方式。

package tests

import (
	"github.com/stretchr/testify/assert"
	"net/http"
	"strconv"
	"testing"
)

func TestAllPages(t *testing.T) {
	baseUrl := "http://localhost:3000"

	// 1. 声明加初始化测试数据
	var tests = []struct {
		method   string
		url      string
		expected int
	}{
		{"GET", "/", http.StatusOK},
		{"GET", "/about", http.StatusOK},
		{"GET", "/notfound", http.StatusNotFound},
		{"GET", "/articles", http.StatusOK},
		{"GET", "/articles/create", http.StatusOK},
		{"GET", "/articles/3", http.StatusOK},
		{"GET", "/articles/3/edit", http.StatusOK},
		{"POST", "/articles/3", http.StatusOK},
		{"POST", "/articles", http.StatusOK},
		{"POST", "/articles/1/delete", http.StatusNotFound},
	}

	// 2. 遍历所有测试
	for _, test := range tests {
		t.Logf("当前请求 URL: %v \n", test.url)

		var (
			resp *http.Response
			err  error
		)

		// 2.1 请求以获取响应
		switch {
		case test.method == "POST":
			data := make(map[string][]string)
			resp, err = http.PostForm(baseUrl+test.url, data)
		default:
			resp, err = http.Get(baseUrl + test.url)
		}
		// 2.2 断言
		assert.NoError(t, err, "请求 "+test.url+" 时报错")
		assert.Equal(t, test.expected, resp.StatusCode, test.url+" 应返回状态码 "+strconv.Itoa(resp.StatusCode))
	}
}

测试结果:

E:\goProject\src\GoBlog\tests>go test -v
=== RUN   TestAllPages
    pages_test.go:33: 当前请求 URL: /
    pages_test.go:33: 当前请求 URL: /about
    pages_test.go:33: 当前请求 URL: /notfound
    pages_test.go:33: 当前请求 URL: /articles
    pages_test.go:33: 当前请求 URL: /articles/create
    pages_test.go:33: 当前请求 URL: /articles/3
    pages_test.go:33: 当前请求 URL: /articles/3/edit
    pages_test.go:33: 当前请求 URL: /articles/3
    pages_test.go:33: 当前请求 URL: /articles
    pages_test.go:33: 当前请求 URL: /articles/1/delete
--- PASS: TestAllPages (0.03s)
PASS
ok      GoBlog/tests    0.143s

testify 常用的断言函数

// 相等
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
// 是否为 nil
func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
// 是否为空
func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
// 是否存在错误
func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool
func Error(t TestingT, err error, msgAndArgs ...interface{}) bool
// 是否为 0 值
func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool
func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool
// 是否为布尔值
func True(t TestingT, value bool, msgAndArgs ...interface{}) bool
func False(t TestingT, value bool, msgAndArgs ...interface{}) bool
// 断言长度一致
func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool
// 断言包含、子集、非子集
func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool
func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool)
func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool)
// 断言文件和目录存在
func FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool
func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool

testing.T

上面的”表组测试“当中我们使用了 t.logf() 方法,GO 标准库当中的 testing 包提供了很多辅助方法:

// 获取测试名称
method (*T) Name() string
// 打印日志
method (*T) Log(args ...interface{})
// 打印日志,支持 Printf 格式化打印
method (*T) Logf(format string, args ...interface{})
// 反馈测试失败,但不退出测试,继续执行
method (*T) Fail()
// 反馈测试失败,立刻退出测试
method (*T) FailNow()
// 反馈测试失败,打印错误
method (*T) Error(args ...interface{})
// 反馈测试失败,打印错误,支持 Printf 的格式化规则
method (*T) Errorf(format string, args ...interface{})
// 检测是否已经发生过错误
method (*T) Failed() bool
// 相当于 Error + FailNow,表示这是非常严重的错误,打印信息结束需立刻退出。
method (*T) Fatal(args ...interface{})
// 相当于 Errorf + FailNow,与 Fatal 类似,区别在于支持 Printf 格式化打印信息;
method (*T) Fatalf(format string, args ...interface{})
// 跳出测试,从调用 SkipNow 退出,如果之前有错误依然提示测试报错
method (*T) SkipNow()
// 相当于 Log 和 SkipNow 的组合
method (*T) Skip(args ...interface{})
// 与Skip,相当于 Logf 和 SkipNow 的组合,区别在于支持 Printf 格式化打印
method (*T) Skipf(format string, args ...interface{})
// 用于标记调用函数为 helper 函数,打印文件信息或日志,不会追溯该函数。
method (*T) Helper()
// 标记测试函数可并行执行,这个并行执行仅仅指的是与其他测试函数并行,相同测试不会并行。
method (*T) Parallel()
// 可用于执行子测试
method (*T) Run(name string, f func(t *T)) bool
赞(0) 打赏
未经允许不得转载:时光日记 » Golang笔记(五):单元测试

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏