如何使用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
相关产品推荐
相关产品推荐

