前言
今天在使用testify框架写单元测试的时候有这样一个需求:对于一个方法来说,可能会有很长的上下文链路数据。 按照正常的单元测试流程,这个时候我们需要按照接口的逻辑来事先mock好原始未处理的数据,并且定义最终想要的数据结果。 定义好不同的test case 尽可能的覆盖到每一个if else,达到一定的覆盖率,才可以通过后续的ci流程。但对于一些特殊的case,我们需要一些特殊的操作:
rust
测试前置处理-> 运行测试代码 -> 测试后处理
需要在测试前后对数据进行预处理,如:事先存入一些数据,测试后再删除这些数据。
这个时候按照官方文档,应该写一个afterEachTest,写在方法TearDownTest,并且绑定在testify默认创建的suit上。
scss
// TearDownTest 在每个测试方法执行后调用,用于清理测试数据
func (ts *LogicsTestSuite) TearDownTest() {
ts.afterEachTest()
}
这个时候就有奇怪的地方了,我并没有在我的测试方法中手动的运行这个TearDownTest,它究竟是如何运行的呢?
问题分析
其实key就在 github.com/stretchr/te... 这里
简化的源代码如下:
go
package main
import (
"fmt"
"reflect"
"strings"
)
// ====== 模拟 testify 的接口定义 ======
// 测试套件级别的接口
type SetupAllSuite interface {
SetupSuite()
}
type TearDownAllSuite interface {
TearDownSuite()
}
// 每个测试级别的接口
type SetupTestSuite interface {
SetupTest()
}
type TearDownTestSuite interface {
TearDownTest()
}
// ====== 模拟你的测试套件 ======
type MyTestSuite struct {
// 注意:这里不需要显式嵌入任何东西
testData string
}
// 你实现了这些方法,就等于实现了对应的接口
func (s *MyTestSuite) SetupSuite() {
fmt.Println("🏠 SetupSuite: 整个测试套件开始前的初始化")
}
func (s *MyTestSuite) TearDownSuite() {
fmt.Println("🏠 TearDownSuite: 整个测试套件结束后的清理")
}
func (s *MyTestSuite) SetupTest() {
fmt.Println(" ⚡ SetupTest: 每个测试开始前的准备")
s.testData = "fresh data"
}
func (s *MyTestSuite) TearDownTest() {
fmt.Println(" ⚡ TearDownTest: 每个测试结束后的清理")
s.testData = ""
}
// 测试方法(必须以 Test 开头)
func (s *MyTestSuite) TestMethod1() {
fmt.Println(" ✅ TestMethod1 执行,测试数据:", s.testData)
}
func (s *MyTestSuite) TestMethod2() {
fmt.Println(" ✅ TestMethod2 执行,测试数据:", s.testData)
}
// ====== 模拟 testify 的 Run 函数 ======
func RunTestSuite(suite interface{}) {
fmt.Println("=== testify.Run() 开始执行 ===")
// 1. 套件级别的钩子
// 注意:这里是 testify 框架在主动调用!
if setupAllSuite, ok := suite.(SetupAllSuite); ok {
setupAllSuite.SetupSuite()
}
// 使用 defer 确保最后调用
if tearDownAllSuite, ok := suite.(TearDownAllSuite); ok {
defer tearDownAllSuite.TearDownSuite()
}
// 2. 通过反射找到所有测试方法
suiteType := reflect.TypeOf(suite)
suiteValue := reflect.ValueOf(suite)
for i := 0; i < suiteType.NumMethod(); i++ {
method := suiteType.Method(i)
// 查找以 Test 开头的方法
if strings.HasPrefix(method.Name, "Test") {
fmt.Printf("\n--- 执行 %s ---\n", method.Name)
// 3. 每个测试的钩子调用
// SetupTest - 测试前
if setupTestSuite, ok := suite.(SetupTestSuite); ok {
setupTestSuite.SetupTest()
}
// 使用 defer 确保测试后调用
if tearDownTestSuite, ok := suite.(TearDownTestSuite); ok {
defer tearDownTestSuite.TearDownTest()
}
// 4. 执行实际的测试方法
method.Func.Call([]reflect.Value{suiteValue})
}
}
fmt.Println("\n=== testify.Run() 执行完成 ===")
}
func main() {
suite := &MyTestSuite{}
RunTestSuite(suite)
}
这里采用了模版方法设计模式。在之前定义了生命周期的相关接口和方法,在Run方法中会使用类型断言来查看是否已经实现了TearDownTest interface,如果实现了,就调用interface中定义的方法。 在go中,对于一个interface ,我们不需要显式的去implement定义它的实现,而是采用非侵入性接口(Implicit Interfaces)+ 结构匹配(Structural Typing)的一种ducking type 的设计方式。 在这里就可以体现这种设计的优势,如果我们使用Java这种需要显式impl的方式那么这里就会这样写:
生命周期定义:
csharp
// 定义多个接口
public interface SetupAllSuite {
void setupSuite();
}
public interface TearDownAllSuite {
void tearDownSuite();
}
public interface SetupTestSuite {
void setupTest();
}
public interface TearDownTestSuite {
void tearDownTest();
}
实现:
csharp
public class MyTestSuite implements
SetupAllSuite, TearDownAllSuite,
SetupTestSuite, TearDownTestSuite {
private String testData;
@Override
public void setupSuite() {
System.out.println("🏠 SetupSuite: 整个测试套件开始前的初始化");
}
@Override
public void tearDownSuite() {
System.out.println("🏠 TearDownSuite: 整个测试套件结束后的清理");
}
@Override
public void setupTest() {
System.out.println(" ⚡ SetupTest: 每个测试开始前的准备");
testData = "fresh data";
}
@Override
public void tearDownTest() {
System.out.println(" ⚡ TearDownTest: 每个测试结束后的清理");
testData = "";
}
public void testMethod1() {
System.out.println(" ✅ TestMethod1 执行,测试数据: " + testData);
}
public void testMethod2() {
System.out.println(" ✅ TestMethod2 执行,测试数据: " + testData);
}
}
这样一比是不是就可以显著看出区别?
gpt 总结了一个表格如下:

总结
这种设计模式的好处在于,它能够在保持整体流程一致的前提下,允许不同的实现类根据自身需求灵活调整具体的执行细节。 通过将通用逻辑上移到抽象父类中,模板方法模式有效地减少了重复手动调用代码,从而提升系统的可维护性与可扩展性。 对于子类,我们只需关注自身差异化的部分,实现起来更加专注且清晰,同时也避免了因修改整体流程而带来的连锁影响。 这种模式让系统在结构上保持稳定,变化部分被局部化处理,变得类似积木一样"可加载",使得逻辑更清晰和简洁。