您现在的位置是:首页 >其他 >golang web学习随便记8-应用测试网站首页其他

golang web学习随便记8-应用测试

sjg20010414 2024-07-17 06:01:01
简介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)。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。