You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Spock框架Mock类中静态变量与Builder并校验参数

Spock实现MyLogger参数校验测试方案

核心实现逻辑

要完成静态常量Mock、链式Builder拦截和参数校验,核心分三步:

  • 替换MyLogger类中静态final修饰的logContext常量,注入自定义Mock实例,避免真实日志上下文初始化
  • 对Builder链式调用做分层Mock,让每个链式方法返回Mock实例自身,避免空指针同时保留方法调用拦截能力
  • 用Spock自带的交互校验语法,断言withObject、withReason方法被调用时传入了预期参数

兼容所有Spock 2.x版本的实现(反射替换静态字段)

不需要额外依赖,直接通过JDK反射修改静态final字段即可,测试代码如下:

import spock.lang.Specification
import java.lang.reflect.Field
import java.lang.reflect.Modifier

class MyLoggerSpec extends Specification {
    // 提前Mock所有链式调用涉及的对象
    LogContext mockLogContext = Mock()
    EventBuilder mockEventBuilder = Mock()
    LogEvent mockLogEvent = Mock()
    MyLogger underTest = new MyLogger()

    def setup() {
        // 1. 反射替换静态final的logContext字段
        Field logContextField = MyLogger.getDeclaredField("logContext")
        logContextField.setAccessible(true)
        // 移除字段的final修饰符
        Field modifiersField = Field.class.getDeclaredField("modifiers")
        modifiersField.setAccessible(true)
        modifiersField.setInt(logContextField, logContextField.getModifiers() & ~Modifier.FINAL)
        logContextField.set(null, mockLogContext)

        // 2. 反射注入@ConfigProperty标记的appVersion测试值
        Field versionField = MyLogger.getDeclaredField("appVersion")
        versionField.setAccessible(true)
        versionField.set(underTest, "test-version-1.0")
    }

    def "调用logIt时,Builder应该传入Token对象和对应失败原因"() {
        given: "配置链式Mock的返回逻辑"
        mockLogContext.eventBuilder(CommonTemplates.FAILED) >> mockEventBuilder
        // 所有Builder链式方法返回mockBuilder自身,实现链式调用
        mockEventBuilder.withObject(_) >> mockEventBuilder
        mockEventBuilder.withReason(_) >> mockEventBuilder
        mockEventBuilder.withDeviceVersion(_) >> mockEventBuilder
        mockEventBuilder.build() >> mockLogEvent

        when: "执行待测试方法"
        underTest.logIt()

        then: "校验目标方法的入参是否符合预期"
        // 校验withObject确实接收到"Token"参数,且仅调用1次
        1 * mockEventBuilder.withObject("Token")
        // 校验withReason确实接收到"Did not work!"参数,且仅调用1次
        1 * mockEventBuilder.withReason("Did not work!")
        // 校验最终日志打印逻辑正常执行
        1 * mockLogEvent.logError(true)
    }
}

Spock 2.4+ 简化写法(内置静态Mock能力)

如果使用2.4及以上版本的Spock,可以直接用内置的@MockStatic注解替换静态方法返回值,不需要手写反射修改字段:

import spock.lang.Specification
import spock.lang.MockStatic
import java.lang.reflect.Field

class MyLoggerSpec extends Specification {
    @MockStatic(LogContext)
    LogContext mockLogContext = Mock()
    EventBuilder mockEventBuilder = Mock()
    LogEvent mockLogEvent = Mock()
    MyLogger underTest = new MyLogger()

    def setup() {
        // 直接Mock静态工厂方法返回Mock实例
        LogContext.createLogContext() >> mockLogContext
        // 注入appVersion测试值
        Field versionField = MyLogger.getDeclaredField("appVersion")
        versionField.setAccessible(true)
        versionField.set(underTest, "test-version-1.0")
    }

    def "调用logIt时,Builder应该传入Token对象和对应失败原因"() {
        given:
        mockLogContext.eventBuilder(CommonTemplates.FAILED) >> mockEventBuilder
        mockEventBuilder.withObject(_) >> mockEventBuilder
        mockEventBuilder.withReason(_) >> mockEventBuilder
        mockEventBuilder.withDeviceVersion(_) >> mockEventBuilder
        mockEventBuilder.build() >> mockLogEvent

        when:
        underTest.logIt()

        then:
        1 * mockEventBuilder.withObject("Token")
        1 * mockEventBuilder.withReason("Did not work!")
        1 * mockLogEvent.logError(true)
    }
}

关键说明

  • 链式Mock的核心是让每个返回Builder的方法都返回Mock的Builder实例,否则链式调用会因为返回null抛出空指针
  • Spock的交互校验次数 * mock对象.方法(预期参数)会自动匹配入参,只要参数不符合预期就会测试失败,完全满足参数校验的需求
  • 测试过程中不会执行真实的日志打印逻辑,不会产生外部依赖副作用

内容的提问来源于stack exchange,提问作者Toni26

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 13:27:25