如何实现可代理标准Runner的自定义JUnit Runner以兼容多框架?
方案:委托模式+自定义注解实现可复用的JUnit Runner
没问题,我帮你设计一个既可以复用MockitoJUnitRunner、SpringRunner等现有Runner的功能,又能在测试方法前后插入自定义逻辑的方案,核心思路是用委托模式+自定义注解来实现,完全不用重复造轮子!
1. 第一步:定义自定义注解@MyConfig
先搞一个注解,用来标记测试类,同时允许你指定要复用的基础Runner(比如Mockito或者Spring的Runner):
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.junit.runner.Runner; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyConfig { // 默认用BlockJUnit4ClassRunner,也可以指定其他Runner Class<? extends Runner> value() default org.junit.runners.BlockJUnit4ClassRunner.class; }
2. 第二步:实现自定义Runner(核心逻辑)
这个Runner不直接写测试执行逻辑,而是委托给你指定的目标Runner,同时通过JUnit的Statement接口来包装测试方法,插入你的自定义前后置代码:
import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; public class MyRunner extends BlockJUnit4ClassRunner { private final Runner delegateRunner; private MyApi api = new MyApi(); public MyRunner(Class<?> klass) throws InitializationError { super(klass); // 读取@MyConfig注解,拿到用户指定的Runner MyConfig config = klass.getAnnotation(MyConfig.class); if (config == null) { throw new InitializationError("测试类必须添加@MyConfig注解哦"); } try { // 实例化用户指定的委托Runner delegateRunner = config.value().getConstructor(Class.class).newInstance(klass); } catch (Exception e) { throw new InitializationError("初始化委托Runner失败:" + e.getMessage()); } } // 把测试名称的获取委托给目标Runner @Override public String getTestName(FrameworkMethod method) { return delegateRunner.getTestName(method); } // 核心:拦截测试方法执行,包装前后置逻辑 @Override protected Statement methodBlock(FrameworkMethod method) { // 先拿到目标Runner原本的执行逻辑 Statement originalStatement = delegateRunner instanceof BlockJUnit4ClassRunner ? ((BlockJUnit4ClassRunner) delegateRunner).methodBlock(method) : super.methodBlock(method); // 包装自定义的前后置操作 return new Statement() { @Override public void evaluate() throws Throwable { // 测试方法执行前的自定义逻辑 api.beforeTestMethod(method.getName()); try { // 执行原有测试逻辑(包括目标Runner的功能,比如Mockito初始化、Spring上下文加载) originalStatement.evaluate(); } finally { // 测试方法执行后的自定义逻辑,不管成功失败都会执行 api.afterTestMethod(method.getName()); } } }; } // 如果需要,也可以把类级别的执行逻辑委托给目标Runner(比如Spring的上下文初始化) @Override protected Statement classBlock() { if (delegateRunner instanceof BlockJUnit4ClassRunner) { return ((BlockJUnit4ClassRunner) delegateRunner).classBlock(); } return super.classBlock(); } }
3. 第三步:使用示例
比如你要结合MockitoJUnitRunner,只需要在测试类上同时加@RunWith(MyRunner.class)和@MyConfig(MockitoJUnitRunner.class):
import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MyRunner.class) @MyConfig(MockitoJUnitRunner.class) public class MockitoTest { @Mock private UserService userService; @Test public void testUserQuery() { // 这里可以正常使用Mockito的Mock对象,同时你的MyApi会在测试前后执行自定义逻辑 } }
如果要结合SpringRunner,只需要把@MyConfig的value改成SpringRunner.class就行:
@RunWith(MyRunner.class) @MyConfig(SpringRunner.class) @SpringBootTest public class SpringTest { @Autowired private OrderService orderService; @Test public void testOrderCreate() { // 正常使用Spring的依赖注入,同时你的自定义前后置逻辑生效 } }
关键知识点说明
- 委托模式的优势:解决了JUnit Runner单继承的限制,不用继承多个Runner就能复用它们的功能,灵活组合不同的Runner特性。
- Statement接口的作用:JUnit用
Statement来封装每一个测试执行步骤,通过包装Statement,我们可以在不修改原有Runner代码的前提下,轻松插入自定义的前后置操作。 - 自定义注解的灵活性:通过
@MyConfig,你可以给不同的测试类指定不同的基础Runner,不用修改MyRunner的代码。
注意事项
- 确保你指定的基础Runner有一个接收
Class<?>参数的构造方法(大多数官方Runner都符合这个要求)。 - 如果需要适配更特殊的Runner(比如Parameterized),可以在MyRunner中添加额外的判断和适配逻辑。
- 你的
MyApi类可以根据需求实现任何自定义逻辑,比如记录测试耗时、清理测试数据、发送测试通知等等。
内容的提问来源于stack exchange,提问作者bobmarksie
相关产品推荐
相关产品推荐

