go-unit-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unit Testing

单元测试

1. Benefits

1. 优势

  • Readability
    Ensures high code quality and reliability. Tests are self-documenting, reducing cognitive load for reviewers and maintainers.
  • Consistency
    Uniform structure across tests ensures predictable, familiar code that team members can navigate efficiently.
  • Scalability
    Table-driven and data-driven approaches minimize boilerplate code when adding new test cases, making it simple to expand coverage.
  • Debuggability
    Scoped traces and detailed assertion messages pinpoint failures quickly during continuous integration and local testing.
  • 可读性
    确保高代码质量和可靠性。测试具备自文档性,降低评审人员和维护人员的认知负担。
  • 一致性
    测试采用统一结构,代码可预测且易于理解,团队成员能高效浏览。
  • 可扩展性
    表格驱动和数据驱动方法在添加新测试用例时最大限度减少样板代码,便于扩展测试覆盖率。
  • 可调试性
    范围化的跟踪信息和详细的断言消息能在持续集成和本地测试中快速定位故障。

2. Patterns

2. 测试模式

2.1. In-Got-Want

2.1. In-Got-Want

The In-Got-Want pattern structures each test case into three clear sections.
  • In
    Defines the input parameters or conditions for the test.
  • Got
    Captures the actual output or result produced by the code under test.
  • Want
    Specifies the expected output or result that the test is verifying against.
In-Got-Want模式将每个测试用例分为三个清晰的部分。
  • In
    定义测试的输入参数或条件。
  • Got
    捕获被测代码产生的实际输出或结果。
  • Want
    指定测试要验证的预期输出或结果。

2.2. Table-Driven Testing

2.2. 表格驱动测试

Table-driven testing organizes test cases in a tabular format, allowing multiple scenarios to be defined concisely.
  • Test Case Structure
    Each row in the table represents a distinct test case with its own set of inputs and expected outputs.
  • Iteration
    The test framework iterates over each row, executing the same test logic with different data.
表格驱动测试以表格形式组织测试用例,可简洁定义多个测试场景。
  • 测试用例结构
    表格中的每一行代表一个独立的测试用例,包含自身的输入和预期输出。
  • 迭代执行
    测试框架遍历每一行,使用不同数据执行相同的测试逻辑。

2.3. Data-Driven Testing (DDT)

2.3. 数据驱动测试(DDT)

Data-driven testing separates test data from test logic, enabling the same test logic to be executed with multiple sets of input data.
  • External Data Sources
    Test data can be stored in external files (e.g., JSON, CSV) and loaded at runtime.
  • Reusability
    The same test logic can be reused with different datasets, enhancing maintainability and coverage.
数据驱动测试将测试数据与测试逻辑分离,使同一测试逻辑可通过多组输入数据执行。
  • 外部数据源
    测试数据可存储在外部文件(如JSON、CSV)中,并在运行时加载。
  • 可复用性
    同一测试逻辑可与不同数据集复用,提升可维护性和测试覆盖率。

2.4. Arrange, Act, Assert (AAA)

2.4. 准备-执行-断言(AAA)

The AAA pattern structures each test case into three clear phases.
  • Arrange
    Set up the necessary preconditions and inputs for the test.
  • Act
    Execute the function or method being tested.
  • Assert
    Verify that the actual output matches the expected output.
AAA模式将每个测试用例分为三个清晰的阶段。
  • Arrange(准备)
    设置测试所需的前置条件和输入。
  • Act(执行)
    执行被测函数或方法。
  • Assert(断言)
    验证实际输出是否与预期输出匹配。

2.5. Test Fixtures

2.5. 测试夹具

Test fixtures provide a consistent and reusable setup and teardown mechanism for test cases.
  • Setup
    Initialize common objects or state needed for multiple tests.
  • Teardown
    Clean up resources or reset state after each test.
测试夹具为测试用例提供一致且可复用的初始化和清理机制。
  • 初始化
    初始化多个测试所需的通用对象或状态。
  • 清理
    在每个测试完成后清理资源或重置状态。

3. Workflow

3. 工作流程

  1. Identify
    Identify new functions in
    pkg/
    or
    internal/
    (e.g.,
    pkg/<package>/<file>.go
    ).
  2. Add/Create
    Create new tests in the same package (e.g.,
    pkg/<package>/<file>_test.go
    ).
  3. Test Coverage Requirements
    Include comprehensive edge cases:
    • Coverage-guided cases
    • Boundary values (min/max limits, edge thresholds)
    • Empty/null inputs
    • Null pointers and invalid references
    • Overflow/underflow scenarios
    • Special cases (negative numbers, zero, special states)
  4. Apply Templates
    Structure all tests using the template pattern.
  1. 识别
    识别
    pkg/
    internal/
    中的新函数(例如:
    pkg/<package>/<file>.go
    )。
  2. 添加/创建
    在同一包中创建新测试(例如:
    pkg/<package>/<file>_test.go
    )。
  3. 测试覆盖率要求
    需包含全面的边缘场景:
    • 覆盖率导向的测试用例
    • 边界值(最小/最大限制、边缘阈值)
    • 空/空值输入
    • 空指针和无效引用
    • 溢出/下溢场景
    • 特殊情况(负数、零、特殊状态)
  4. 应用模板
    使用模板模式构建所有测试。

4. Commands

4. 命令

CommandDescription
make go-test-unit
Execute tests with race detection and JUnit report
make go-test-coverage
Generate coverage reports (HTML and XML)
命令描述
make go-test-unit
启用竞争检测并生成JUnit报告的测试执行命令
make go-test-coverage
生成覆盖率报告(HTML和XML格式)

5. Style Guide

5. 风格指南

  • Test Framework
    Use the standard Go
    testing
    package.
  • Include Imports
    Include
    testing
    and
    github.com/google/go-cmp/cmp
    for comparisons.
  • Parallelism
    Use
    t.Parallel()
    to run tests in parallel.
  • Test Organization
    Consolidate test cases for a single function into one
    TestXxx(t *testing.T)
    function
    using table-driven testing.
    This approach:
    • Eliminates redundant test function definitions
    • Simplifies maintenance by grouping related scenarios together
    • Reduces code duplication in setup and teardown phases
    • Makes it easier to add or modify test cases
  • Assertions
    Use
    cmp.Equal
    for value comparisons and
    errors.Is
    for error checking.
  • 测试框架
    使用Go标准
    testing
    包。
  • 导入依赖
    导入
    testing
    github.com/google/go-cmp/cmp
    用于值比较。
  • 并行执行
    使用
    t.Parallel()
    实现测试并行运行。
  • 测试组织
    使用表格驱动测试,将单个函数的所有测试用例整合到一个
    TestXxx(t *testing.T)
    函数
    中。
    该方法的优势:
    • 消除冗余的测试函数定义
    • 通过分组相关场景简化维护
    • 减少初始化和清理阶段的代码重复
    • 便于添加或修改测试用例
  • 断言方式
    使用
    cmp.Equal
    进行值比较,使用
    errors.Is
    进行错误检查。

6. Template

6. 模板

Use these templates for new unit tests. Replace placeholders with actual values.
使用以下模板创建新单元测试。将占位符替换为实际值。

6.1. File Header Template

6.1. 文件头模板

go
// SPDX-License-Identifier: Apache-2.0

package <package>

import (
	"errors"
	"testing"

	"github.com/google/go-cmp/cmp"
)
go
// SPDX-License-Identifier: Apache-2.0

package <package>

import (
	"errors"
	"testing"

	"github.com/google/go-cmp/cmp"
)

6.2. Table-Driven Test Template

6.2. 表格驱动测试模板

go
func Test<FunctionName>(t *testing.T) {
	t.Parallel()

	// In-Got-Want
	type in struct {
		/* input fields */
	}

	type want struct {
		/* expected output fields */
		err error
	}

	// Table-Driven Testing
	tests := []struct {
		name string
		in   in
		want want
	}{
		{
			name: "case-description-1",
			in: in{
				/* input values */
			},
			want: want{
				/* expected output */
				err: nil,
			},
		},
		{
			name: "case-description-2",
			in: in{
				/* input values */
			},
			want: want{
				/* expected output */
				err: nil, // or specific error
			},
		},
		// add more cases as needed
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Arrange
			// additional setup as needed

			// Act
			got, err := <Function>(tt.in.<input>)

			// Assert
			if !errors.Is(err, tt.want.err) {
				t.Errorf("<Function>() error = %v, want err %v", err, tt.want.err)
			}
			if !cmp.Equal(got, tt.want.<value>) {
				t.Errorf("<Function>(%+v) = %v, want %v", tt.in, got, tt.want.<value>)
			}
		})
	}
}
go
func Test<FunctionName>(t *testing.T) {
	t.Parallel()

	// In-Got-Want
	type in struct {
		/* input fields */
	}

	type want struct {
		/* expected output fields */
		err error
	}

	// Table-Driven Testing
	tests := []struct {
		name string
		in   in
		want want
	}{
		{
			name: "case-description-1",
			in: in{
				/* input values */
			},
			want: want{
				/* expected output */
				err: nil,
			},
		},
		{
			name: "case-description-2",
			in: in{
				/* input values */
			},
			want: want{
				/* expected output */
				err: nil, // or specific error
			},
		},
		// add more cases as needed
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Arrange
			// additional setup as needed

			// Act
			got, err := <Function>(tt.in.<input>)

			// Assert
			if !errors.Is(err, tt.want.err) {
				t.Errorf("<Function>() error = %v, want err %v", err, tt.want.err)
			}
			if !cmp.Equal(got, tt.want.<value>) {
				t.Errorf("<Function>(%+v) = %v, want %v", tt.in, got, tt.want.<value>)
			}
		})
	}
}

6.3. Test Fixture Template

6.3. 测试夹具模板

go
// testFixture holds common test state and provides setup/teardown.
type testFixture struct {
	t      *testing.T
	// Add common fields for test state
	object *<Type>
}

// newTestFixture creates and initializes a test fixture.
func newTestFixture(t *testing.T) *testFixture {
	t.Helper()

	// Setup
	return &testFixture{
		t:      t,
		object: New<Type>(),
	}
}

// teardown cleans up resources after test completion.
func (f *testFixture) teardown() {
	f.t.Helper()

	// Teardown
	if f.object != nil {
		f.object.Close()
	}
}

func Test<FunctionName>WithFixture(t *testing.T) {
	t.Parallel()

	// Arrange
	f := newTestFixture(t)
	defer f.teardown()

	input := <input_value>

	// Act
	got, err := f.object.<Function>(input)

	// Assert
	if err != nil {
		t.Errorf("<Function>() unexpected error: %v", err)
	}
	if !cmp.Equal(got, <expected>) {
		t.Errorf("<Function>() = %v, want %v", got, <expected>)
	}
}
go
// testFixture holds common test state and provides setup/teardown.
type testFixture struct {
	t      *testing.T
	// Add common fields for test state
	object *<Type>
}

// newTestFixture creates and initializes a test fixture.
func newTestFixture(t *testing.T) *testFixture {
	t.Helper()

	// Setup
	return &testFixture{
		t:      t,
		object: New<Type>(),
	}
}

// teardown cleans up resources after test completion.
func (f *testFixture) teardown() {
	f.t.Helper()

	// Teardown
	if f.object != nil {
		f.object.Close()
	}
}

func Test<FunctionName>WithFixture(t *testing.T) {
	t.Parallel()

	// Arrange
	f := newTestFixture(t)
	defer f.teardown()

	input := <input_value>

	// Act
	got, err := f.object.<Function>(input)

	// Assert
	if err != nil {
		t.Errorf("<Function>() unexpected error: %v", err)
	}
	if !cmp.Equal(got, <expected>) {
		t.Errorf("<Function>() = %v, want %v", got, <expected>)
	}
}

6.4. Error Test Template

6.4. 错误测试模板

go
func Test<FunctionName>Error(t *testing.T) {
	t.Parallel()

	// In-Got-Want
	type in struct {
		/* invalid input fields */
	}

	type want struct {
		err error
	}

	// Table-Driven Testing
	tests := []struct {
		name string
		in   in
		want want
	}{
		{
			name: "nil-input-returns-error",
			in: in{
				/* nil or invalid input */
			},
			want: want{
				err: resource.Err<ErrorName>,
			},
		},
		{
			name: "invalid-value-returns-error",
			in: in{
				/* invalid value */
			},
			want: want{
				err: resource.Err<ErrorName>,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Arrange
			// setup if needed

			// Act
			_, err := <Function>(tt.in.<input>)

			// Assert
			if !errors.Is(err, tt.want.err) {
				t.Errorf("<Function>() error = %v, want err %v", err, tt.want.err)
			}
		})
	}
}
go
func Test<FunctionName>Error(t *testing.T) {
	t.Parallel()

	// In-Got-Want
	type in struct {
		/* invalid input fields */
	}

	type want struct {
		err error
	}

	// Table-Driven Testing
	tests := []struct {
		name string
		in   in
		want want
	}{
		{
			name: "nil-input-returns-error",
			in: in{
				/* nil or invalid input */
			},
			want: want{
				err: resource.Err<ErrorName>,
			},
		},
		{
			name: "invalid-value-returns-error",
			in: in{
				/* invalid value */
			},
			want: want{
				err: resource.Err<ErrorName>,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Arrange
			// setup if needed

			// Act
			_, err := <Function>(tt.in.<input>)

			// Assert
			if !errors.Is(err, tt.want.err) {
				t.Errorf("<Function>() error = %v, want err %v", err, tt.want.err)
			}
		})
	}
}

6.5. Boundary Value Test Template

6.5. 边界值测试模板

go
func Test<FunctionName>BoundaryValues(t *testing.T) {
	t.Parallel()

	// In-Got-Want
	type in struct {
		input <input_type>
	}

	type want struct {
		value <output_type>
		err   error
	}

	// Table-Driven Testing
	tests := []struct {
		name string
		in   in
		want want
	}{
		{
			name: "minimum-value",
			in:   in{input: <MIN_VALUE>},
			want: want{value: /* expected */, err: nil},
		},
		{
			name: "maximum-value",
			in:   in{input: <MAX_VALUE>},
			want: want{value: /* expected */, err: nil},
		},
		{
			name: "zero-value",
			in:   in{input: 0},
			want: want{value: /* expected */, err: nil},
		},
		{
			name: "negative-value",
			in:   in{input: -1},
			want: want{value: /* expected */, err: nil},
		},
		{
			name: "overflow-value",
			in:   in{input: math.MaxFloat64},
			want: want{value: 0, err: resource.ErrOverflow},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Arrange
			// setup if needed

			// Act
			got, err := <Function>(tt.in.input)

			// Assert
			if !errors.Is(err, tt.want.err) {
				t.Errorf("<Function>() error = %v, want err %v", err, tt.want.err)
			}
			if !cmp.Equal(got, tt.want.value) {
				t.Errorf("<Function>(%v) = %v, want %v", tt.in.input, got, tt.want.value)
			}
		})
	}
}
go
func Test<FunctionName>BoundaryValues(t *testing.T) {
	t.Parallel()

	// In-Got-Want
	type in struct {
		input <input_type>
	}

	type want struct {
		value <output_type>
		err   error
	}

	// Table-Driven Testing
	tests := []struct {
		name string
		in   in
		want want
	}{
		{
			name: "minimum-value",
			in:   in{input: <MIN_VALUE>},
			want: want{value: /* expected */, err: nil},
		},
		{
			name: "maximum-value",
			in:   in{input: <MAX_VALUE>},
			want: want{value: /* expected */, err: nil},
		},
		{
			name: "zero-value",
			in:   in{input: 0},
			want: want{value: /* expected */, err: nil},
		},
		{
			name: "negative-value",
			in:   in{input: -1},
			want: want{value: /* expected */, err: nil},
		},
		{
			name: "overflow-value",
			in:   in{input: math.MaxFloat64},
			want: want{value: 0, err: resource.ErrOverflow},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Arrange
			// setup if needed

			// Act
			got, err := <Function>(tt.in.input)

			// Assert
			if !errors.Is(err, tt.want.err) {
				t.Errorf("<Function>() error = %v, want err %v", err, tt.want.err)
			}
			if !cmp.Equal(got, tt.want.value) {
				t.Errorf("<Function>(%v) = %v, want %v", tt.in.input, got, tt.want.value)
			}
		})
	}
}

6.6. Data-Driven Test Template (JSON)

6.6. 数据驱动测试模板(JSON)

go
import (
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/google/go-cmp/cmp"
)

// testCase represents a single test case loaded from JSON.
type testCase struct {
	Name string `json:"name"`
	In   struct {
		Input <input_type> `json:"input"`
	} `json:"in"`
	Want struct {
		Expected <output_type> `json:"expected"`
	} `json:"want"`
}

// testData represents the JSON test data structure.
type testData struct {
	Tests []testCase `json:"tests"`
}

func Test<FunctionName>DataDriven(t *testing.T) {
	t.Parallel()

	// Load test data from JSON file
	testdataPath := filepath.Join("testdata", "<function>_test.json")
	data, err := os.ReadFile(testdataPath)
	if err != nil {
		t.Fatalf("failed to read test data: %v", err)
	}

	var td testData
	if err := json.Unmarshal(data, &td); err != nil {
		t.Fatalf("failed to parse test data: %v", err)
	}

	for _, tc := range td.Tests {
		t.Run(tc.Name, func(t *testing.T) {
			// Arrange
			input := tc.In.Input
			expected := tc.Want.Expected

			// Act
			got, err := <Function>(input)

			// Assert
			if err != nil {
				t.Errorf("<Function>() unexpected error: %v", err)
			}
			if !cmp.Equal(got, expected) {
				t.Errorf("<Function>(%v) = %v, want %v", input, got, expected)
			}
		})
	}
}
  • tests/data/<function>_test.json
    JSON file containing test cases.
    json
    {
      "tests": [
        {
          "name": "case-description-1",
          "in": {
            "input": <value>
          },
          "want": {
            "expected": <value>
          }
        },
        {
          "name": "case-description-2",
          "in": {
            "input": <value>
          },
          "want": {
            "expected": <value>
          }
        }
      ]
    }
go
import (
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/google/go-cmp/cmp"
)

// testCase represents a single test case loaded from JSON.
type testCase struct {
	Name string `json:"name"`
	In   struct {
		Input <input_type> `json:"input"`
	} `json:"in"`
	Want struct {
		Expected <output_type> `json:"expected"`
	} `json:"want"`
}

// testData represents the JSON test data structure.
type testData struct {
	Tests []testCase `json:"tests"`
}

func Test<FunctionName>DataDriven(t *testing.T) {
	t.Parallel()

	// Load test data from JSON file
	testdataPath := filepath.Join("testdata", "<function>_test.json")
	data, err := os.ReadFile(testdataPath)
	if err != nil {
		t.Fatalf("failed to read test data: %v", err)
	}

	var td testData
	if err := json.Unmarshal(data, &td); err != nil {
		t.Fatalf("failed to parse test data: %v", err)
	}

	for _, tc := range td.Tests {
		t.Run(tc.Name, func(t *testing.T) {
			// Arrange
			input := tc.In.Input
			expected := tc.Want.Expected

			// Act
			got, err := <Function>(input)

			// Assert
			if err != nil {
				t.Errorf("<Function>() unexpected error: %v", err)
			}
			if !cmp.Equal(got, expected) {
				t.Errorf("<Function>(%v) = %v, want %v", input, got, expected)
			}
		})
	}
}
  • tests/data/<function>_test.json
    包含测试用例的JSON文件。
    json
    {
      "tests": [
        {
          "name": "case-description-1",
          "in": {
            "input": <value>
          },
          "want": {
            "expected": <value>
          }
        },
        {
          "name": "case-description-2",
          "in": {
            "input": <value>
          },
          "want": {
            "expected": <value>
          }
        }
      ]
    }

7. References

7. 参考资料

  • Go Testing package documentation.
  • Google go-cmp package documentation.