在GO工程实践中,测试是一个至关重要的环节,它关系到系统的质量和稳定性。
基础认知
一、测试类型
- 回归测试:
- 通常由QA(质量保证)人员手动通过终端对固定的主流程场景进行回归测试。
- 集成测试:
- 对系统功能维度进行测试验证,通过服务暴露的某个接口进行自动化测试。层级从上至下,测试成本逐渐降低,而测试覆盖率逐步上升。
- 单元测试:
- 开发者对单独的函数、模块做功能验证。单元测试主要包括输入、测试单元、输出以及校对。用最后的校对来保证代码的功能与预期相符。
- 单元测试可以保证质量,在整体覆盖率足够的情况下,既保证了新功能本身的正确性,又未破坏原有代码的正确性。同时,单元测试可以提升效率,在代码有bug的情况下,通过编写单元测试,可以在一个较短周期内定位和修复问题。
- 编写单元测试时需要遵循一些基本规范,例如测试文件以“_test.go”结尾,测试函数以“Test”开头且连接的第一个字母大写,初始化逻辑可以放到“TestMain”中。
二、单元测试的具体实践
- 测试代码的独立性:
- 每个测试用例都应该是独立的,不依赖于其他测试用例的状态或结果。这样做的目的是为了保证测试结果的一致性,确保能准确地定位问题。
- 边界条件的测试:
- 测试不仅应涵盖常规条件,还要包括边界和异常情况,这有助于确保代码在各种情况下都能正常工作。
- 使用表驱动测试:
- 表驱动测试是一种结构化的测试方法,可以用不同的测试数据多次运行同一个测试逻辑。
- 测试覆盖率:
- 覆盖率是衡量代码是否经过了足够测试的重要指标。通过提高单元测试覆盖率,可以评价项目的测试水准,评估项目是否达到了高水准测试等级。例如,一个判断是否及格的函数,最初覆盖率为66.7%,通过增加一个不及格的测试用例,重新执行所有单元测试,最终可以将覆盖率提升至100%。
- 依赖管理:
- 对于工程中复杂的项目,一般会依赖数据库、缓存等,而单元测试需要保证稳定性和幂等性(每一次测试运行都应该产生与之前一样的结果)。为了实现这一目的,可以使用mock机制。例如,使用monkey这样的开源mock测试库,可以对方法或者实例进行mock,从而摆脱对本地文件或其他外部资源的依赖。
三、基准测试
基准测试是指测试一段程序的运行性能及耗费CPU的程度。在实际项目开发中,经常会遇到代码性能瓶颈,需要利用基准测试对代码做性能分析来定位问题。Go语言内置的测试框架提供了基准测试的能力。
例如,对于一个随机选择服务器的函数,可以进行基准测试来评估其性能。在并发情况下,如果发现性能存在劣化,可以尝试使用开源的高性能随机数方法(如fastrand)来优化代码。
案例
单元测试
// calculator.go
func Add(a, b int) int { return a + b }
- 基本单元测试
import "testing"
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1, 2) = %d; expected %d", result, expected)
}
}
运行测试:
go test -v
- 表驱动测试
func TestAddMultiple(t *testing.T) {
tests := []struct {
a, b, expected int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
{1, -1, 0},
}
for _, tt := range tests {
actual := Add(tt.a, tt.b)
if actual != tt.expected {
t.Errorf("Add(%d, %d) = %d; expected %d", tt.a, tt.b, actual, tt.expected)
}
}
}
以下是一个需要从文件中读取数据的函数ReadFile,为了进行单元测试,可以使用mock来避免依赖实际的文件系统
// original_function.go
package main
import ( "fmt" "io/ioutil" )
func ReadFile(filename string) (string, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
return string(data), nil
}
使用monkey库进行mock:
import ( "github.com/bouk/monkey" "testing" )
func TestReadFileMock(t *testing.T) {
originalFunction := func(filename string) (string, error) {
return "mocked data", nil
}
patch := monkey.Patch(ReadFile, originalFunction)
defer patch.Unpatch()
result, err := ReadFile("non_existent_file.txt")
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
if result != "mocked data" {
t.Errorf("Expected 'mocked data', got: %s", result)
}
}
基准测试
以下是一个简单的基准测试例子,用于测量一个字符串拼接操作的性能:
// benchmark_test.go
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "hello" + " " + "world"
}
}
运行基准测试:
go test -bench=.
集成测试
以下是一个测试HTTP服务器的处理函数的例子:
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":8080", nil)
}
接下来,创建一个名为main_test.go的文件,用于编写集成测试:
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHelloHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(helloHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := "Hello, World!"
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
在集成测试中,我们将处理函数与请求和响应记录器关联起来,并调用ServeHTTP方法来执行处理函数。最后,我们检查响应的状态码和正文是否符合预期。
运行测试:
go test