您现在的位置是:首页 >其他 >golang web学习随便记8-应用测试网站首页其他
golang web学习随便记8-应用测试
golang标准库中和测试有关的基础设施有:testing包及基于它的net/http/httptest包、go test 命令。
testing包要求测试文件的名字以 _test.go后缀结尾,一般测试文件名字和被测试源码文件是对应的(如 server_test.go 文件测试的是 server.go 文件中的函数或功能),并且测试文件和被测试文件位于同一个包内。
在测试文件内部使用的测试函数,必须是以下格式的函数(这里指的是功能测试,基准测试函数格式见后),函数内部可以使用 Error、Errorf、Fail、FailNow、Log、Logf、Skip等方法表示测试失败、记录到日志或跳过测试(Fatal = Log + FailNow,Fatalf=Logf + FailNow):
func TestXxx(t *testing.T) {
// ............
t.Error(..)
// ............
t.Fail(..)
// ............
t.Skip(..)
}
当用户在终端执行 go test 命令时,所有 TestXxx 测试函数就会被执行。
基本测试方法
下面的测试例子,建立在项目 jsonparse 基础上(golang web学习随便记7-XML、JSON、Web服务_sjg20010414的博客-CSDN博客 decoder方式解析),把解码的功能打包到函数 decode 中,然后函数 decode 作为我们测试的目标函数。
// ......................................................
func decode(filename string) (post Post, err error) {
jsonFile, err := os.Open(filename) // 文件名改成参数
if err != nil {
fmt.Println("打开JSON文件出错:", err)
return
}
defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
for {
// var post Post
// err := decoder.Decode(&post)
err = decoder.Decode(&post)
if err == io.EOF {
break
}
if err != nil {
fmt.Println("解码JSON出错:", err)
return
}
// fmt.Println(post) // 打印输出非常耗时
}
return // 返回
}
func main() {
_, err := decode("post.json") // 改成函数调用
if err != nil && err != io.EOF {
fmt.Println("Error: ", err)
}
}
创建文件 main_test.go,对 main.go 文件进行测试(主要就是测试 decode 函数,因此测试函数命名成了 TestDecode):
package main
import (
"io"
"testing"
)
func TestDecode(t *testing.T) {
post, err := decode("post.json")
if err != nil && err != io.EOF {
t.Error(err)
}
if post.Id != 1 {
t.Error("错误的 id, 期望得到 1 但得到的值为 ", post.Id)
}
if post.Content != "你好,Golang" {
t.Error("错误的内容, 期望得到 '你好,Golang' 但得到的为 ", post.Content)
}
}
func TestEncode(t *testing.T) {
t.Skip("暂时跳过编码函数的测试")
}
运行测试用例的方式类似如下:(显然,我们可以从 VSCode等 IDE 直接点击函数左侧三角形来运行单个测试函数)
sjg@sjg-PC:~/go/src/jsonparse$ go test
{1 你好,Golang {2 张三} [{1 C++才是好语言 {3 李四}} {2 Rust比C++好 {4 王五}}]}
PASS
ok jsonparse 0.001s
sjg@sjg-PC:~/go/src/jsonparse$ go test -v -cover
=== RUN TestDecode
{1 你好,Golang {2 张三} [{1 C++才是好语言 {3 李四}} {2 Rust比C++好 {4 王五}}]}
--- PASS: TestDecode (0.00s)
=== RUN TestEncode
main_test.go:22: 暂时跳过编码函数的测试
--- SKIP: TestEncode (0.00s)
PASS
jsonparse coverage: 61.1% of statements
ok jsonparse 0.003s
我们添加一个测试函数,用来模拟非常耗时的测试过程:
func TestLongRunningTest(t *testing.T) {
if testing.Short() {
t.Skip("在 短时模式 下跳过长时间运行的测试")
}
time.Sleep(10 * time.Second)
}
上述测试函数中,用 testing.Short() 判断测试是否处于短时模式,如果处于短时模式,就跳过当前测试函数。分别用 go test -v -cover 和 go test -v -cover -short 测试就能体会这种差别。
并行地运行测试
对于不存在依赖的单元,golang可以实现并行地测试(在测试函数中调用 t.Parallel()即可)。下面的例子用3个耗时分别为1秒、2秒、3秒的测试函数来演示并行运行测试的方法,编写 parallel_test.go 如下:
package main
import (
"testing"
"time"
)
func TestParallelCostOneSec(t *testing.T) {
t.Parallel()
time.Sleep(1 * time.Second)
}
func TestParallelCostTwoSec(t *testing.T) {
t.Parallel()
time.Sleep(2 * time.Second)
}
func TestParallelCostThreeSec(t *testing.T) {
t.Parallel()
time.Sleep(3 * time.Second)
}
分别用三条命令运行测试如下(观察总耗时): -parallel 参数可以指定最多并行运行几个测试用例
sjg@sjg-PC:~/go/src/jsonparse$ go test -v -short
........................................
=== CONT TestParallelCostOneSec
=== CONT TestParallelCostTwoSec
=== CONT TestParallelCostThreeSec
--- PASS: TestParallelCostOneSec (1.00s)
--- PASS: TestParallelCostTwoSec (2.00s)
--- PASS: TestParallelCostThreeSec (3.00s)
PASS
ok jsonparse 3.002s
sjg@sjg-PC:~/go/src/jsonparse$ go test -v -short -parallel 3
................................................
=== CONT TestParallelCostOneSec
=== CONT TestParallelCostTwoSec
=== CONT TestParallelCostThreeSec
--- PASS: TestParallelCostOneSec (1.00s)
--- PASS: TestParallelCostTwoSec (2.00s)
--- PASS: TestParallelCostThreeSec (3.00s)
PASS
ok jsonparse 3.003s
sjg@sjg-PC:~/go/src/jsonparse$ go test -v -short -parallel 2
............................................
=== CONT TestParallelCostOneSec
=== CONT TestParallelCostTwoSec
--- PASS: TestParallelCostOneSec (1.00s)
=== CONT TestParallelCostThreeSec
--- PASS: TestParallelCostTwoSec (2.00s)
--- PASS: TestParallelCostThreeSec (3.00s)
PASS
ok jsonparse 4.004s
基准测试
和单元测试一样,基准测试文件也是以 _test.go 为后缀;和单元测试不同的是,每个基准测试函数格式如下:
func BenchmarkDecode(b *testing.B) {
for i := 0; i < b.N; i++ { // 执行 b.N 次作为基准
// 待测试的操作
}
}
然后运行go test命令时,添加 -bench <regexpr> 参数表示要执行指定的基准测试文件,<regexpr> 为 . 表示目录下的所有基准测试文件。
测试函数耗时
编写如下bench_test.go 文件:
package main
import "testing"
func BenchmarkDecode(b *testing.B) {
for i := 0; i < b.N; i++ {
decode("post.json")
}
}
用命令 sjg@sjg-PC:~/go/src/jsonparse$ go test -v -cover -short -bench . 运行。
注意:在基准测试中,测试用例的迭代次数是由golang自己确定的,用户只能限制运行时间,不能精确控制迭代次数。默认golang会迭代足够多的次数来获得比较准确的结果。
注意:上面的这条命令,功能测试也会一并执行,如果要跳过功能测试,方法是用 -run 参数指定一个不存在的功能测试,从而功能测试被跳过,即 go test -run notexist -bench .
sjg@sjg-PC:~/go/src/jsonparse$ go test -run notexist -bench .
goos: linux
goarch: amd64
pkg: jsonparse
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkDecode-8 100562 10268 ns/op
PASS
ok jsonparse 1.156s
对比函数耗时
基准测试还能帮我们判断哪个函数运行更快(当然,判断完成同一件事哪个更快才有意义)。我们先把 golang web学习随便记7-XML、JSON、Web服务_sjg20010414的博客-CSDN博客
中 Unmarshal 方式的 json 解码部分重构成 unmarshal 函数放入 main.go(书作者的decode函数是只解码一个post的,少掉判断和循环,因此时间最短。书作者的decode我的代码里命名为decode2):
func decode2(filename string) (post Post, err error) {
jsonFile, err := os.Open(filename)
if err != nil {
fmt.Println("Error opening JSON file:", err)
return
}
defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
err = decoder.Decode(&post)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
return
}
func unmarshal(filename string) (post Post, err error) {
jsonFile, err := os.Open(filename) // 文件名改成参数
if err != nil {
fmt.Println("打开JSON文件出错:", err)
return
}
defer jsonFile.Close()
jsonData, err := ioutil.ReadAll(jsonFile)
if err != nil {
fmt.Println("读取JSON数据出错:", err)
return
}
// var post Post
json.Unmarshal(jsonData, &post)
// fmt.Println(post)
return // 返回
}
然后在 bench_test.go 文件中添加如下测试函数:
func BenchmarkDecode2(b *testing.B) {
for i := 0; i < b.N; i++ {
decode2("post.json")
}
}
func BenchmarkUnmarshal(b *testing.B) {
for i := 0; i < b.N; i++ {
unmarshal("post.json")
}
}
运行结果如下:(该结果从侧面证明,对于不大的JSON数据,用 Unmarshal 就可以了)
sjg@sjg-PC:~/go/src/jsonparse$ go test -run notexist -bench .
goos: linux
goarch: amd64
pkg: jsonparse
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkDecode-8 105285 10177 ns/op
BenchmarkDecode2-8 146533 7826 ns/op
BenchmarkUnmarshal-8 146584 7885 ns/op
PASS
ok jsonparse 3.660s
HTTP测试
前面的测试,属于通用的单元测试和基准测试,对于 golang web,经常需要的是 HTTP 测试,即模拟客户发起HTTP请求,获取模拟服务器返回的HTTP响应。下面的代码,用了前面编写的web服务的例子(golang web学习随便记7-XML、JSON、Web服务_sjg20010414的博客-CSDN博客Web服务)。
基本的 GET、PUT测试
在web服务项目中,创建 main_test.go 测试文件:
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHandleGet(t *testing.T) {
mux := http.NewServeMux() // 用于运行测试的多路复用器
mux.HandleFunc("/post/", handleRequest) // 绑定待测 handler
writer := httptest.NewRecorder() // 服务器响应都“录制”到writer
request, _ := http.NewRequest("GET", "/post/1", nil)
mux.ServeHTTP(writer, request) // request请求后,响应的数据 writer 负责
if writer.Code != 200 {
t.Errorf("响应代码 %v", writer.Code)
}
var post Post
json.Unmarshal(writer.Body.Bytes(), &post)
if post.Id != 1 {
t.Error("未能获取 JSON post")
}
}
对于测试单个请求,上面代码中的多路复用器似乎无必要,它在后面的多个请求在一起的测试中才体现价值。上面的测试代码表明,HTTP测试时,测试文件自己就是服务器,只是这个服务器比较特殊,它会把请求后服务器的响应都录制下来,然后对录制的结果进行检验。
记得先从docker启动mariadb 10.3服务器,然后 go test 运行该测试:(500是数据库服务器没开时的结果)
sjg@sjg-PC:~/go/src/webservice$ go test
--- FAIL: TestHandleGet (0.00s)
main_test.go:19: 响应代码 500
main_test.go:24: 未能获取 JSON post
FAIL
exit status 1
FAIL webservice 0.003s
sjg@sjg-PC:~/go/src/webservice$ go test
PASS
ok webservice 0.005s
我们在 main_test.go 中添加一个测试函数:
func TestHandlePut(t *testing.T) {
mux := http.NewServeMux() // 用于运行测试的多路复用器
mux.HandleFunc("/post/", handleRequest) // 绑定待测 handler
writer := httptest.NewRecorder() // 服务器响应都“录制”到writer
json := strings.NewReader(`{"content":"C++老矣,尚能打否","author":"李四"}`)
request, _ := http.NewRequest("PUT", "/post/1", json)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("响应代码 %v", writer.Code)
}
}
下面的命令可以单独测试这个函数(然而,你要验证数据改了没有,只能去数据库里看,或者修改前面的测试来验证数据):
sjg@sjg-PC:~/go/src/webservice$ go test -run "TestHandlePut"
PASS
ok webservice 0.005s
从 TestHandleGet 和 TestHandlePut 我们可以发现有不少代码是重复的,如果有更多测试函数,重复带来的累赘会更明显。对此,testing包支持用 TestMain 函数和setUp、tearDown机制来对测试工作进行预设和拆卸工作。典型的 TestMain 函数结构如下:
func TestMain(m *testing.M) {
setUp()
code := m.Run()
tearDown()
os.Exit(code)
}
func setUp() {
// 初始化或预设一些东西
}
func tearDown() {
// 拆卸一些东西
}
使用 TestMain 机制,我们可以改写 main_test.go 如下:
// ..........................................
var mux *http.ServeMux
var writer *httptest.ResponseRecorder
func TestMain(m *testing.M) {
setUp()
code := m.Run()
os.Exit(code)
}
func setUp() {
mux = http.NewServeMux()
mux.HandleFunc("/post/", handleRequest)
writer = httptest.NewRecorder()
}
func TestHandleGet(t *testing.T) {
// mux := http.NewServeMux() // 用于运行测试的多路复用器
// mux.HandleFunc("/post/", handleRequest) // 绑定待测 handler
// writer := httptest.NewRecorder() // 服务器响应都“录制”到writer
// ................................
}
func TestHandlePut(t *testing.T) {
// mux := http.NewServeMux() // 用于运行测试的多路复用器
// mux.HandleFunc("/post/", handleRequest) // 绑定待测 handler
// writer := httptest.NewRecorder() // 服务器响应都“录制”到writer
// ................................
}
测试替身与依赖注入
前面的 GET、PUT 测试展示的测试功能,只能测试web服务的多路复用器和处理器,差不做相当于 controller 层,它无法覆盖数据层(相当于Model和Service)。