Skip to main content

Go test

Golang 单元测试对文件名和方法名,参数都有很严格的要求。

  1. 文件名必须以xx_test.go命名
  2. 方法必须是Test[^a-z]开头
  3. 方法参数必须 t *testing.T
  4. 使用go test执行单元测试

Setup 与 TearDown

在编写测试时,通常需要在每个测试运行之前进行一些初始化设置(Setup),并在测试运行之后进行一些清理工作(TearDown)。虽然 Go 的 testing 包没有直接提供类似于其他测试框架(如 JUnit)的 SetupTearDown 方法,但我们可以通过一些技巧来实现相同的功能。

使用 testing.TCleanup 方法

Go 1.14 引入了 testing.TCleanup 方法,使得在测试结束时执行清理工作变得更加方便。Cleanup 方法接受一个函数,该函数会在测试结束时被调用。

示例

以下是一个使用 Cleanup 方法的示例:

package example

import (
"testing"
)

// Setup function to initialize resources.
func setup(t *testing.T) func() {
t.Log("Setup")
// Return a function to clean up resources.
return func() {
t.Log("TearDown")
}
}

func TestAdd(t *testing.T) {
// Call setup and defer the cleanup function.
cleanup := setup(t)
t.Cleanup(cleanup)

result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1, 2) = %d; want %d", result, expected)
}
}

在这个示例中,setup 函数执行初始化工作并返回一个清理函数。在测试函数 TestAdd 中,我们调用 setup 并将返回的清理函数传递给 t.Cleanup,确保它在测试结束时被调用。

使用 TestMain 进行全局设置和清理

如果需要在所有测试运行之前和之后进行全局的设置和清理,可以使用 TestMain 函数。TestMain 允许你在运行所有测试之前进行初始化,并在所有测试运行之后进行清理。

示例

以下是一个使用 TestMain 进行全局设置和清理的示例:

package example

import (
"os"
"testing"
)

func setup() {
// Perform setup tasks
println("Global Setup")
}

func teardown() {
// Perform teardown tasks
println("Global TearDown")
}

func TestMain(m *testing.M) {
setup()
code := m.Run() // Run the tests
teardown()
os.Exit(code)
}

func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1, 2) = %d; want %d", result, expected)
}
}

在这个示例中,TestMain 函数在所有测试运行之前调用 setup,并在所有测试运行之后调用 teardownm.Run() 运行所有测试并返回退出代码。

总结

  • 使用 testing.TCleanup 方法可以在每个测试结束时执行清理工作。
  • 使用 TestMain 函数可以在所有测试运行之前和之后进行全局的设置和清理。
  • 通过这些方法,你可以实现类似于其他测试框架中的 SetupTearDown 功能。

希望这些信息能帮助你更好地管理测试的初始化和清理工作。如果你有任何其他问题或需要进一步的解释,请随时提问。

Setup 与 TearDown 设置上下文

你说得对,局部变量因为作用域的限制,通常在函数内部处理即可,不需要特别的 Setup 和 TearDown 方法来管理。但在某些情况下,确实需要在每个测试函数中共享一些状态或资源,这时我们可以通过结构体和方法来组织代码,使得 Setup 和 TearDown 更加灵活和模块化。

使用结构体和方法实现 Setup 和 TearDown

我们可以定义一个结构体来封装共享的状态或资源,并为这个结构体添加 Setup 和 TearDown 方法。每个测试函数都可以创建这个结构体的实例,并调用其方法来进行初始化和清理。

以下是一个示例:

package example

import (
"testing"
)

// TestContext is a structure that holds shared state for tests.
type TestContext struct {
t *testing.T
sharedVar int
}

// Setup initializes the shared state.
func (ctx *TestContext) Setup() {
ctx.t.Log("Setup")
ctx.sharedVar = 42
}

// TearDown cleans up the shared state.
func (ctx *TestContext) TearDown() {
ctx.t.Log("TearDown")
ctx.sharedVar = 0
}

func TestAdd(t *testing.T) {
ctx := &TestContext{t: t}
ctx.Setup()
defer ctx.TearDown()

if ctx.sharedVar != 42 {
t.Errorf("Expected sharedVar to be 42, got %d", ctx.sharedVar)
}

result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1, 2) = %d; want %d", result, expected)
}
}

func TestSubtract(t *testing.T) {
ctx := &TestContext{t: t}
ctx.Setup()
defer ctx.TearDown()

if ctx.sharedVar != 42 {
t.Errorf("Expected sharedVar to be 42, got %d", ctx.sharedVar)
}

result := Subtract(5, 3)
expected := 2
if result != expected {
t.Errorf("Subtract(5, 3) = %d; want %d", result, expected)
}
}

在这个示例中,我们定义了一个 TestContext 结构体来封装共享的状态 sharedVarSetup 方法初始化这个状态,TearDown 方法清理这个状态。每个测试函数创建一个 TestContext 实例,并调用 SetupTearDown 方法来管理共享状态。

总结

通过使用结构体和方法,我们可以更灵活地管理测试中的共享状态和资源,不再局限于全局变量。这种方法不仅提高了代码的可读性和可维护性,还使得测试更加模块化。

希望这个示例能帮助你更好地理解如何在 Go 的测试中实现 Setup 和 TearDown。如果你有任何其他问题或需要进一步的解释,请随时提问。

子测试有点像分组测试

确实,子测试在某种程度上可以看作是分组测试的一种实现方式。它们允许你将相关的测试逻辑组织在一起,并且每个子测试都有自己的名字和作用域。这样做的好处是可以提高代码的可读性和可维护性,同时也方便共享 Setup 和 TearDown 逻辑。

子测试的主要特点

  1. 分组测试:子测试可以将相关的测试逻辑分组在一起,使得代码更加结构化和易于理解。
  2. 独立运行:每个子测试都是独立运行的,一个子测试的失败不会影响其他子测试的执行。
  3. 独立报告:每个子测试的结果都会单独报告,方便调试和问题定位。
  4. 共享 Setup 和 TearDown 逻辑:子测试可以共享父测试的初始化和清理逻辑,减少重复代码。

示例:分组测试

假设我们有一组函数需要测试,如 AddSubtract,我们可以使用子测试来进行分组测试。

package example

import (
"testing"
)

func Add(a, b int) int {
return a + b
}

func Subtract(a, b int) int {
return a - b
}

func TestMathOperations(t *testing.T) {
// Setup
t.Log("Setup for TestMathOperations")

t.Run("Add", func(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1, 2) = %d; want %d", result, expected)
}
})

t.Run("Subtract", func(t *testing.T) {
result := Subtract(5, 3)
expected := 2
if result != expected {
t.Errorf("Subtract(5, 3) = %d; want %d", result, expected)
}
})

// TearDown
t.Log("TearDown for TestMathOperations")
}

在这个示例中,TestMathOperations 函数包含两个子测试,分别测试 AddSubtract 函数。这样可以清晰地看到每个子测试的目的和结果。

使用表驱动测试进行参数化

子测试也非常适合用于表驱动测试,即使用不同的输入参数运行相同的测试逻辑。

func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b, expected int
}{
{"Add1", 1, 2, 3},
{"Add2", 2, 3, 5},
{"Add3", -1, 1, 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}

在这个示例中,我们定义了一个测试用例的数组,并使用子测试来运行每个测试用例。

总结

子测试在 Go 的测试框架中提供了灵活性和结构化的好处,使得测试代码更加清晰、可维护,并且便于调试和报告。通过使用子测试,你可以更好地组织测试逻辑,进行参数化测试,并共享 Setup 和 TearDown 逻辑。

如果你有任何其他问题或需要进一步的解释,请随时提问。

example

当然,以下是一些具体的函数示例以及它们的子测试,这些示例涵盖了基本的数学操作和字符串操作。

示例函数

我们将定义一些简单的数学和字符串操作函数:

package example

// Add returns the sum of two integers.
func Add(a, b int) int {
return a + b
}

// Subtract returns the difference of two integers.
func Subtract(a, b int) int {
return a - b
}

// Multiply returns the product of two integers.
func Multiply(a, b int) int {
return a * b
}

// Concatenate returns the concatenation of two strings.
func Concatenate(a, b string) string {
return a + b
}

子测试示例

接下来,我们将为这些函数编写子测试。

package example

import (
"testing"
)

func TestMathOperations(t *testing.T) {
t.Run("Add", func(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Add(1, 2) = %d; want %d", result, expected)
}
})

t.Run("Subtract", func(t *testing.T) {
result := Subtract(5, 3)
expected := 2
if result != expected {
t.Errorf("Subtract(5, 3) = %d; want %d", result, expected)
}
})

t.Run("Multiply", func(t *testing.T) {
result := Multiply(2, 3)
expected := 6
if result != expected {
t.Errorf("Multiply(2, 3) = %d; want %d", result, expected)
}
})
}

func TestStringOperations(t *testing.T) {
t.Run("Concatenate", func(t *testing.T) {
result := Concatenate("Hello, ", "World!")
expected := "Hello, World!"
if result != expected {
t.Errorf("Concatenate(\"Hello, \", \"World!\") = %s; want %s", result, expected)
}
})
}

func TestAddWithTableDriven(t *testing.T) {
tests := []struct {
name string
a, b, expected int
}{
{"Add1", 1, 2, 3},
{"Add2", 2, 3, 5},
{"Add3", -1, 1, 0},
}

for _, tt := range tests {
tt := tt // capture range variable
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}

解释

  1. TestMathOperations:这个测试函数包含三个子测试,分别测试 AddSubtractMultiply 函数。每个子测试都有自己的作用域和名字,可以独立运行和报告。

  2. TestStringOperations:这个测试函数包含一个子测试,用于测试 Concatenate 函数。

  3. TestAddWithTableDriven:这个测试函数使用表驱动测试方法,定义了一组测试用例,并使用子测试来运行每个测试用例。这样可以方便地测试 Add 函数在不同输入下的表现。

运行测试

你可以使用 go test 命令来运行这些测试:

go test -v

-v 标志表示详细模式,会输出每个子测试的结果。

总结

通过这些示例,你可以看到如何使用子测试来组织和运行相关的测试逻辑。子测试提高了代码的可读性和可维护性,使得测试结果更加清晰和易于调试。如果你有任何其他问题或需要进一步的解释,请随时提问。