Skip to main content

Command Palette

Search for a command to run...

Go 隐式接口与模板方法

Updated
Go 隐式接口与模板方法

前言

今天在使用testify框架写单元测试的时候有这样一个需求: 对于一个方法来说,可能会有很长的上下文链路数据。 按照正常的单元测试流程,这个时候我们需要按照接口的逻辑来事先mock好原始未处理的数据,并且定义最终想要的数据结果。定义好不同的test case 尽可能的覆盖到每一个if else,才可以通过后续的ci 流程。 对于一些特殊的case,我们需要一些特殊的操作:

测试前置处理-> 运行测试代码 -> 测试后处理

需要在测试前后对数据进行预处理,如:事先存入一些数据,测试后再删除这些数据。

这个时候按照官方文档,应该写一个afterEachTest,写在方法TearDownTest,并且绑定在testify默认创建的suit上。

// TearDownTest 在每个测试方法执行后调用,用于清理测试数据
func (ts *LogicsTestSuite) TearDownTest() {
    ts.afterEachTest()
}

这个时候就有奇怪的地方了,我并没有在我的测试方法中手动的运行这个TearDownTest,它究竟是如何运行的呢?

问题分析

其实key就在 https://github.com/stretchr/testify/blob/master/suite/suite.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的方式那么这里就会这样写:

生命周期定义:

// 定义多个接口
public interface SetupAllSuite {
    void setupSuite();
}

public interface TearDownAllSuite {
    void tearDownSuite();
}

public interface SetupTestSuite {
    void setupTest();
}

public interface TearDownTestSuite {
    void tearDownTest();
}

实现:

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 总结了一个表格如下:

总结

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

Reference

https://github.com/stretchr/testify https://refactoringguru.cn/design-patterns/template-method

More from this blog

一次Nginx 403 的问题排查

前言&问题复现 参与了一个内部效率提升项目(边角料项目)后要发到内部的测试机器上。内部的测试机器上并没有配置集群,没有一个专门的ingress或者说是网关来处理请求分发。 并且这个测试机器属于多个部门,导致机器环境很复杂,一台物理机安装有多个nginx,有直接host安装的,也有在容器上运行的。 由于这是一个内部项目,没有必要专门部署一个minio,but 项目需要上传文件,所以就直接保存在server的目录下,简单配置了一下nginx的配置,配置如下: location /static...

Oct 18, 2025
一次Nginx 403 的问题排查

[Learn With Agent] JSX & React Components

前言 这是Learn With Agent 的第一篇博客,这个系列(不知道有没有后续了)是想通过agent辅助进行快速的学习(过概念)掌握一些相关的知识。之后再通过agent辅助开发,应该也能做出来个7788。对于AI还有很多思考,另起一个博客再说吧就。 因为网络和经费原因,这里使用的是CC+GLM 4.6 & Gemini 系列。🈚️广,文字为百分百人类手敲&传统CV大法。 JSX JSX是React生态中占据很重要的部分,JSX是一种语法拓展,它可以允许用户在js中写类似html的标签结构...

Oct 9, 2025
[Learn With Agent] JSX & React Components

提示词以及常见优化技巧

prompt 种类 system : 系统提示词用于统一设定当前会话下ai的行为,例如设定ai的行为、语气、风格或限制。用户不会显式观察到系统提示词,但是会影响到用户和ai的后续对话。 user : 这是用户实际向ai提出的问题,这是对话的起点。 assistant: 这是ai对于用户问题的回答,通常也会被添加到上下文中,供下一次对话进行参考,所以也属于prompt的范畴。 prompt 格式 标准格式: <Instruction> 问答格式: <Question>? 零样本提示 ...

Aug 14, 2025
提示词以及常见优化技巧

Http 长连接 & 短连接详解

网络连接基础 TCP/IP TCP/IP,是几乎所有互联网通信的基石。HTTP、WebSocket 和 常见的RPC框架尽管功能各异,但都运行在应用层,并从根本上依赖传输层的 TCP(传输控制协议)来实现可靠的、面向连接的数据传输 。 TCP 的核心职责是确保数据包从发送端到接收端可靠、按序且无损地传输 。这包括序列号、确认、流量控制和拥塞控制等机制。在网络层,IP(互联网协议)负责网络路由和寻址,使数据能够跨越不同网络到达其目的地 。现代操作系统普遍内置并管理 TCP/IP 协议栈,从而为应用...

Jun 13, 2025
E

Ekreke's Blog

11 posts