设计模式与测试:Mock中的代理、测试框架中的模板方法、断言中的策略

说实话,很多程序员写业务代码时设计模式用得飞起,一到写测试就全忘了。我见过不少团队,生产代码架构清晰,测试代码却是一锅粥。其实测试代码也是代码,同样需要设计模式来指导。今天我们就聊聊测试中三个最常见的模式应用场景。

Mock中的代理模式

Mock框架,说白了就是造假的。你想想看,测试一个订单服务,总不能真的去调支付网关吧?那就要用Mock来模拟外部依赖。

Mock框架的核心原理是什么?就是代理模式。我常用的Mockito,底层就是通过动态代理生成一个假的实现类。你调用它的方法,它不执行真实逻辑,而是返回你预设的结果。

// Java示例:Mockito中的代理
public class OrderServiceTest {
    
    @Test
    public void testCreateOrder() {
        // 这里就是代理模式的应用
        PaymentGateway mockGateway = Mockito.mock(PaymentGateway.class);
        
        // 预设行为 - 这就是代理对象的方法拦截
        Mockito.when(mockGateway.charge(Mockito.anyDouble()))
               .thenReturn(true);
        
        OrderService service = new OrderService(mockGateway);
        Order order = service.createOrder(100.0);
        
        Assertions.assertNotNull(order);
        Assertions.assertEquals("PAID", order.getStatus());
    }
}

我个人习惯把Mock对象看作「替身演员」。真实对象太贵(调外部服务)、太慢(数据库查询)、或者有副作用(发邮件),那就找个替身来演。代理模式在这里干的事就是:拦截所有方法调用,返回你事先准备好的结果。

核心要点:Mock框架使用动态代理(JDK动态代理或CGLIB)创建代理对象。代理对象拦截方法调用,根据预设的桩代码(stubbing)返回结果。这就是代理模式在测试中的经典应用。

我曾经接手过一个项目,测试跑一次要40分钟。为什么?因为每个测试都真的去调数据库、调外部API。后来我把所有外部依赖都Mock掉,测试时间降到了3分钟。嗯,这就是代理模式的价值。

测试框架中的模板方法模式

JUnit和TestNG这类测试框架,你有没有想过它们是怎么控制测试流程的?比如@BeforeEach、@AfterEach这些注解,背后的设计思想就是模板方法模式。

模板方法模式定义了一个算法的骨架,把一些步骤延迟到子类中实现。测试框架就是典型的例子:它定义好了「测试执行流程」这个骨架,你只需要填充具体的测试逻辑。

// Java示例:JUnit中的模板方法
public abstract class TestCase {
    
    // 这就是模板方法 - 定义了测试执行的骨架
    public final void run() {
        setUp();      // @BeforeEach
        runTest();    // 具体的测试方法
        tearDown();   // @AfterEach
    }
    
    // 钩子方法 - 子类可以覆盖
    protected void setUp() {
        // 默认空实现
    }
    
    // 抽象方法 - 子类必须实现
    protected abstract void runTest();
    
    protected void tearDown() {
        // 默认空实现
    }
}

// 具体测试类
public class UserServiceTest extends TestCase {
    
    private UserService userService;
    
    @Override
    protected void setUp() {
        // 初始化资源
        userService = new UserService();
        Database.connect();
    }
    
    @Override
    protected void runTest() {
        User user = userService.findById(1L);
        Assertions.assertNotNull(user);
    }
    
    @Override
    protected void tearDown() {
        // 清理资源
        Database.disconnect();
    }
}

你想想看,如果没有模板方法,每个测试类都要自己写setup和teardown的逻辑,那得多乱?框架把「什么时候初始化、什么时候执行、什么时候清理」这个流程固定下来,你只需要关注「测什么」就行了。

个人经验:我在写集成测试时,经常在setUp里创建测试数据,在tearDown里清理数据。有一次忘了清理,导致后续测试全挂了。后来我干脆把数据清理逻辑写到了@AfterEach里,这就是模板方法给我的安全感——流程是固定的,不会漏掉步骤。

断言中的策略模式

断言(Assertion)看起来简单,不就是判断两个值是否相等吗?但实际项目中,断言的方式千差万别:精确匹配、模糊匹配、正则匹配、自定义匹配器……这就是策略模式的用武之地。

AssertJ和Hamcrest这类断言库,核心就是策略模式。每个匹配器(Matcher)就是一个策略,断言方法根据不同的匹配器执行不同的比较逻辑。

// Java示例:AssertJ中的策略模式
public class AssertionTest {
    
    @Test
    public void testStrategyPattern() {
        String result = "Hello, World!";
        
        // 不同的断言策略
        assertThat(result)
            .isNotNull()                    // 非空策略
            .startsWith("Hello")            // 前缀匹配策略
            .contains("World")              // 包含策略
            .matches("[A-Z].+[!]");         // 正则策略
        
        // 自定义策略
        assertThat(result)
            .satisfies(str -> {
                assertThat(str.length()).isGreaterThan(5);
                assertThat(str).doesNotContain("Error");
            });
    }
}

为什么会这样设计?因为断言的需求太灵活了。你不能让框架把所有可能的比较方式都写死,那就让用户自己选择策略。每个Matcher封装一种比较算法,断言方法只负责「执行策略并报告结果」。

避坑指南:我曾经在项目中看到有人自己写了一大堆if-else来做断言,代码又臭又长。其实用AssertJ的软断言(SoftAssertions)就能优雅解决。软断言本质上就是策略模式的一个变体——它把多个断言策略收集起来,最后一次性报告所有失败。

三者如何协同工作

这三个模式在测试中不是孤立的。我画了一张图,帮你理清它们的关系:

测试中的设计模式协同 模板方法模式:测试执行骨架 @BeforeEach → @Test → @AfterEach 测试方法内部 调用Mock对象 + 执行断言 代理模式 Mock对象 = 代理对象 策略模式 断言 = 选择匹配策略 模板方法控制流程 → 代理模式隔离依赖 → 策略模式灵活断言

从上图你能看到,一个完整的测试用例是这样工作的:

  1. 模板方法控制测试的生命周期——什么时候初始化、什么时候执行、什么时候清理
  2. 在测试方法内部,代理模式创建的Mock对象帮你隔离外部依赖
  3. 最后用策略模式的断言来验证结果

说白了,这三个模式各司其职:模板方法管流程,代理模式管隔离,策略模式管验证。我在实际项目中,写测试代码时脑子里会过一遍这个框架,这样写出来的测试既清晰又容易维护。

总结一下:测试代码不是二等公民,它同样需要设计模式来指导。Mock框架用代理模式隔离依赖,测试框架用模板方法固定流程,断言库用策略模式灵活验证。理解这三个模式,你的测试代码质量会上一个台阶。

嗯,今天就聊到这里。下次你写测试的时候,不妨想想:我是不是在用代理模式?模板方法帮我省了哪些重复代码?断言用哪种策略最合适?想清楚了再动手,事半功倍。