Spring Boot单元测试:如何处理链式构建Timer的方法及优化可测性?
处理Spring Boot指标记录方法的单元测试问题
一、绕过现有代码的测试方案
1. 用Mockito Spy重写protected方法
因为目标方法是protected,可创建被测类的Spy对象,重写该方法为空实现:
// 创建被测类的Spy实例 YourTestClass spyInstance = Mockito.spy(new YourTestClass()); // 重写指标记录方法,执行空逻辑 Mockito.doNothing().when(spyInstance).recordRepositoryInvocationMetrics( any(Method.class), any(Object.class), anyLong() );
测试时该指标记录逻辑不会触发,直接跳过链式调用的Mock麻烦。
2. 用PowerMock忽略方法执行
借助PowerMock直接禁止该方法执行,无需处理内部依赖:
@RunWith(PowerMockRunner.class) @PrepareForTest(YourTestClass.class) public class YourTest { @Test public void testYourMethod() { PowerMock.suppress(PowerMock.method(YourTestClass.class, "recordRepositoryInvocationMetrics")); // 执行核心测试逻辑 } }
3. Mock核心依赖避免链式调用
提前Mock meterRegistry 和 context 依赖,拦截Timer的注册与执行:
@Mock private MeterRegistry meterRegistry; @InjectMocks private YourTestClass testInstance; @Test public void testMethod() { Timer mockTimer = Mockito.mock(Timer.class); when(meterRegistry.timer(any(Timer.Id.class))).thenReturn(mockTimer); // 执行测试逻辑 }
此方式需处理链式调用的细节,相对繁琐,优先级低于前两种方案。
二、重构代码提升可测试性
链式调用的核心问题是Timer构建、注册、执行逻辑耦合,拆分后可大幅降低Mock难度:
1. 抽离Timer构建逻辑为独立方法
将Timer的构建逻辑拆分为单独方法,测试时Mock该方法返回预设Timer:
// 重构后的被测类 public class YourClass { private MeterRegistry meterRegistry; private ApplicationContext context; protected void recordRepositoryInvocationMetrics(Method method, Object target, long queryDuration) { Timer timer = buildRepositoryTimer(method, target); timer.record(queryDuration, TimeUnit.MILLISECONDS); } // 抽离的Timer构建方法 protected Timer buildRepositoryTimer(Method method, Object target) { return Timer.builder("mysql.call.duration") .description("MySqL Database call Duration") .tag("method.name", target.getClass().getSimpleName() + "." + method.getName()) .tag("service.name", context.getId() != null ? context.getId() : "unknown") .register(meterRegistry); } }
测试时用Spy重写构建方法:
YourClass spy = Mockito.spy(new YourClass()); Timer mockTimer = Mockito.mock(Timer.class); doReturn(mockTimer).when(spy).buildRepositoryTimer(any(Method.class), any(Object.class)); // 执行测试,触发的record逻辑将调用Mock的Timer
2. 引入Timer工厂类
将Timer创建逻辑完全抽离到独立工厂类,被测类依赖工厂而非直接构建Timer:
// Timer工厂类 @Component public class RepositoryTimerFactory { private MeterRegistry meterRegistry; private ApplicationContext context; public RepositoryTimerFactory(MeterRegistry meterRegistry, ApplicationContext context) { this.meterRegistry = meterRegistry; this.context = context; } public Timer createTimer(Method method, Object target) { return Timer.builder("mysql.call.duration") .description("MySqL Database call Duration") .tag("method.name", target.getClass().getSimpleName() + "." + method.getName()) .tag("service.name", context.getId() != null ? context.getId() : "unknown") .register(meterRegistry); } } // 重构后的被测类 public class YourClass { private final RepositoryTimerFactory timerFactory; public YourClass(RepositoryTimerFactory timerFactory) { this.timerFactory = timerFactory; } protected void recordRepositoryInvocationMetrics(Method method, Object target, long queryDuration) { Timer timer = timerFactory.createTimer(method, target); timer.record(queryDuration, TimeUnit.MILLISECONDS); } }
测试时直接Mock工厂类:
@Mock private RepositoryTimerFactory timerFactory; @Mock private Timer mockTimer; @InjectMocks private YourClass testInstance; @Test public void testMethod() { when(timerFactory.createTimer(any(Method.class), any(Object.class))).thenReturn(mockTimer); // 执行核心测试逻辑 }
3. 直接注入Timer实例(适合固定指标场景)
若Timer标签无需动态生成,可直接将Timer作为依赖注入:
@Component public class YourClass { private final Timer repositoryTimer; public YourClass(MeterRegistry meterRegistry, ApplicationContext context) { this.repositoryTimer = Timer.builder("mysql.call.duration") .description("MySqL Database call Duration") .tag("service.name", context.getId() != null ? context.getId() : "unknown") .register(meterRegistry); } protected void recordRepositoryInvocationMetrics(Method method, Object target, long queryDuration) { repositoryTimer.record(queryDuration, TimeUnit.MILLISECONDS); } }
测试时直接Mock Timer实例即可,完全无需关心构建逻辑。
内容的提问来源于stack exchange,提问作者mal
相关产品推荐
相关产品推荐

