Golang测试入门

124 阅读5分钟

在GO工程实践中,测试是一个至关重要的环节,它关系到系统的质量和稳定性。

基础认知

一、测试类型

  1. 回归测试
    • 通常由QA(质量保证)人员手动通过终端对固定的主流程场景进行回归测试。
  2. 集成测试
    • 对系统功能维度进行测试验证,通过服务暴露的某个接口进行自动化测试。层级从上至下,测试成本逐渐降低,而测试覆盖率逐步上升。
  3. 单元测试
    • 开发者对单独的函数、模块做功能验证。单元测试主要包括输入、测试单元、输出以及校对。用最后的校对来保证代码的功能与预期相符。
    • 单元测试可以保证质量,在整体覆盖率足够的情况下,既保证了新功能本身的正确性,又未破坏原有代码的正确性。同时,单元测试可以提升效率,在代码有bug的情况下,通过编写单元测试,可以在一个较短周期内定位和修复问题。
    • 编写单元测试时需要遵循一些基本规范,例如测试文件以“_test.go”结尾,测试函数以“Test”开头且连接的第一个字母大写,初始化逻辑可以放到“TestMain”中。

二、单元测试的具体实践

  1. 测试代码的独立性
    • 每个测试用例都应该是独立的,不依赖于其他测试用例的状态或结果。这样做的目的是为了保证测试结果的一致性,确保能准确地定位问题。
  2. 边界条件的测试
    • 测试不仅应涵盖常规条件,还要包括边界和异常情况,这有助于确保代码在各种情况下都能正常工作。
  3. 使用表驱动测试
    • 表驱动测试是一种结构化的测试方法,可以用不同的测试数据多次运行同一个测试逻辑。
  4. 测试覆盖率
    • 覆盖率是衡量代码是否经过了足够测试的重要指标。通过提高单元测试覆盖率,可以评价项目的测试水准,评估项目是否达到了高水准测试等级。例如,一个判断是否及格的函数,最初覆盖率为66.7%,通过增加一个不及格的测试用例,重新执行所有单元测试,最终可以将覆盖率提升至100%。
  5. 依赖管理
    • 对于工程中复杂的项目,一般会依赖数据库、缓存等,而单元测试需要保证稳定性和幂等性(每一次测试运行都应该产生与之前一样的结果)。为了实现这一目的,可以使用mock机制。例如,使用monkey这样的开源mock测试库,可以对方法或者实例进行mock,从而摆脱对本地文件或其他外部资源的依赖。

三、基准测试

基准测试是指测试一段程序的运行性能及耗费CPU的程度。在实际项目开发中,经常会遇到代码性能瓶颈,需要利用基准测试对代码做性能分析来定位问题。Go语言内置的测试框架提供了基准测试的能力。

例如,对于一个随机选择服务器的函数,可以进行基准测试来评估其性能。在并发情况下,如果发现性能存在劣化,可以尝试使用开源的高性能随机数方法(如fastrand)来优化代码。

案例

单元测试

// calculator.go  
func Add(a, b int) int { return a + b }
  1. 基本单元测试
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
  1. 表驱动测试
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