Go编写测试(1)
まさか?测试是这样的么...
- 代码测试从上到下分回归测试,集成测试和单元测试。单元测试是最细化,也是覆盖率最大的测试方法,同时成本也最低
单元测试的规则
- 测试文件用**_test.go结尾,例如对main函数编写的测试文件可以命名为main_test.go**
- 测试文件内的对应函数以Test开头,例如对函数Add编写的测试单元命名为TestAdd,传递参数时使用(t *testing.T)
- 测试文件提供了TestMain函数,需要使用到(m *tesing.M),如下
func TestMain(m *tesing.M) {
//测试前:数据装载、配置初始化等
code := m.Run()
//测试后:释放资源等
os.Exit(code)
}
需要注意的是,如果原本函数用到了fmt等标准输出,调用他们只会正常地输出到shell里,此时我们需要重定向,使输出被拦截,才能用于检验,详见下文代码覆盖率
进行测试
- 简单的办法是直接用内置的函数进行测试,比如这个例子,我们在主函数里这样写:
func HelloTom() string {
return "Jerry"
}
再到测试文件:
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
运行 go test -v,得到输出
=== RUN TestHelloTom
main_test.go:9: Expected Tom do not match actual Jerry
--- FAIL: TestHelloTom (0.00s)
FAIL
exit status 1
FAIL Test 0.088s
- Go同样也有一些用于测试的包,比如**testify**,安装路径如下,
go get github.com/stretchr/testify/assert
- 对于上面那样简单的例子,可以使用相等性断言assert.Equal,
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
同样会报错
--- FAIL: TestHelloTom (0.00s)
main_test.go:11:
Error Trace: C:/Users/SUMiRE/Desktop/Test/main_test.go:11
Error: Not equal:
expected: "Tom"
actual : "Jerry"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Tom
+Jerry
Test: TestHelloTom
FAIL
exit status 1
FAIL Test 2.491s
- 在经历了九九八十一难之后我们修正了Tom不是Tom的bug,再次运行测试文件,
PASS
ok Test 2.696s
这样就能完成一个简单的单元测试任务
- 接下来是比较复杂的情况,还记得之前写过的在线词典吗,对于这种依赖外部服务,数据库或其他组件,而我们由于环境搭建,执行耗时或结果不可控等原因不想调用外部依赖时,就可以通过创建模拟对象来替代真实的依赖.这里的重点是
1 package main
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io/ioutil"
8 "log"
9 "net/http"
10 "os"
11 "testing"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/mock"
14 )
15
16 // 定义一个模拟的http.Client结构体,继承自mock.Mock
17 type MockHttpClient struct {
18 mock.Mock
19 }
20
21 // 模拟Do方法,使其返回我们预设的值
22 func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) {
23 args := m.Called(req)
24 return args.Get(0).(*http.Response), args.Error(1)
25 }
26
27 func TestQuery(t *testing.T) {
28 // 创建模拟的http.Client实例
29 mockClient := &MockHttpClient{}
30
31 // 构造一个模拟的HTTP响应,这里假设一个简单的成功响应示例,你可以根据实际情况调整内容
32 mockResponseBody := []byte(`{
33 "rc": 0,
34 "wiki": {},
35 "dictionary": {
36 "prons": {
37 "en-us": "mockUS",
38 "en": "mockUK"
39 },
40 "explanations": ["mock explanation 1", "mock explanation 2"],
41 "synonym": [],
42 "antonym": [],
43 "wqx_example": [],
44 "entry": "",
45 "type": "",
46 "related": [],
47 "source": ""
48 }
49 }`)
50 mockResponse := &http.Response{
51 StatusCode: 200,
52 Body: ioutil.NopCloser(bytes.NewBuffer(mockResponseBody)),
53 }
54
55 // 设置模拟客户端的Do方法在被调用时返回我们构造的模拟响应
56 mockClient.On("Do", mock.Anything).Return(mockResponse, nil)
57
58 // 替换真实的http.Client为模拟的客户端
59 originalClient := http.Client{}
60 http.Client = *mockClient
61
62 // 调用要测试的query函数,传入一个测试单词
63 query("testWord")
64
65 // 恢复原来的http.Client,避免影响其他测试用例(如果有的话)
66 http.Client = originalClient
67
68 // 进行断言验证,例如验证是否正确打印了模拟的发音信息
69 assert.Equal(t, "testWord UK: mockUK US: mockUS", fmt.Sprintf("%s UK: %s US: %s", "testWord", "mockUK", "mockUS"))
70 // 还可以继续添加更多断言来验证对解释信息等的处理是否符合预期
71 assert.Equal(t, "mock explanation 1", "mock explanation 1")
72 assert.Equal(t, "mock explanation 2", "mock explanation 2")
73 }
- 对一些注意点做讲解
- 17-19 定义一个模拟结构体,此处用mock.Mock自动生成,我们会用这个"假"client进行原本client需要进行的发出http请求工作
- 22-25 由于原本http.client会用到Do发出这个请求,我们需要模拟这个方法以拦截原本的请求,获取到返回值以检验
- 23 called()方法会记录调用,并按照用On()定义的模拟返回来写入args
- 24 **Get(x)方法表示从模拟返回的第x索引处获取,mock.Mock不知道需要返回什么,所以它会统一返回interface{}类型,所以要用后面的.(*http.Response)**进行类型断言,表示返回值的类型.**Error(x)**方法同样是从第x索引获取,只是表明值是error类型
- 56 **On()**方法设置调用Do且输入为任意(mock.Anything)时返回设定好的返回
- 59 存好咱们的老client避免找不回来
- 善用Mock可以解决依赖方面的疑难杂症,提高测试独立性,也可以避免可爱的小爬虫被KO
检验测试的有效性---代码覆盖率
- 有没有设置好足够完备的测试数据使大部分代码被检验是有效性的一大保证,为了得知测试文件对原函数的覆盖率,我们先稍微改写一下HelloTom
package main
func HelloTom(condition bool) string {
if condition {
return "Tom"
}
return "Not Tom"
}
func main() {
HelloTom(true)
}
也对测试文件动动手脚
func TestHelloJerry(t *testing.T) {
output := HelloTom(false)
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
然后使用这段命令
go test -cover
得到
--- FAIL: TestHelloJerry (0.00s)
main_test.go:11:
Error Trace: C:/Users/SUMiRE/Desktop/Test/main_test.go:11
Error: Not equal:
expected: "Tom"
actual : "Not Tom"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Tom
+Not Tom
Test: TestHelloJerry
FAIL
coverage: 50.0% of statements
exit status 1
FAIL Test 0.095s
可以看到只有**50%**的覆盖率,说明我们的模拟是不充分的,稍微改写一下测试文件,
func TestHelloJerry(t *testing.T) {
output := HelloTom(false)
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
func TestHelloTom(t *testing.T) {
output := HelloTom(true)
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
这次的结果呢?
--- FAIL: TestHelloJerry (0.00s)
main_test.go:11:
Error Trace: C:/Users/SUMiRE/Desktop/Test/main_test.go:11
Error: Not equal:
expected: "Tom"
actual : "Not Tom"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Tom
+Not Tom
Test: TestHelloJerry
FAIL
coverage: 75.0% of statements
exit status 1
FAIL Test 0.080s
明显变高了,因为我们加入了对条件true相关代码的检验
大问题!我是个完美主义者,这么简单的代码为什么达不到100%????? 检查原代码,发现唯一没有运行到的是main函数,正好对Testmain做下说明
1 func Testmain(t *testing.T) { 2 // 保存标准输出,用于后续恢复 3 old := os.Stdout 4 defer func() { 5 os.Stdout = old 6 }() 7 r, w, \_ := io.Pipe() 8 os.Stdout = w 9 // 模拟调用main函数 10 main() 11 w\.Close() 12 result, \_ := io.ReadAll(r) 13 output := string(result) 14 // 根据main函数实际行为设置期望输出 15 expectOutput := "Tom" 16 assert.Equal(t, expectOutput, output) 17 }3 os.Stdout代表标准输出,我们需要先保留,用于测试函数结束后的恢复 7 **io.Pipe()**创建一个管道类型,返回两个**io.ReadWriter**类型的接口,我们把输出定向到写端***w*** 11 调用完main函数后需要关闭写端 后面的之前都讲过
要想获取具体情况,可以使用更加强大的工具
go test -coverprofile coverage.out
这段代码测试后会生成覆盖率数据文件coverage,out,用
go tool cover -html coverage.out
可以打开html网页,
- 一次合格的测试应该至少达到50% ~ 60%,对于金融等敏感领域甚至需要达到80%
- 单元测试的部分,差不多就到这里
今日推荐:一碗半泡面