使用过spring,然后我们都知道如何使用spring的junit扩展来进行测试,所以我觉得很有必要研究下自定义运行器。比如下面的代码:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({ "/spring-mvc-servlet.xml", "/applicationContext-db.xml" }) @WebAppConfiguration public class FlowServletTest extends BaseTest { @Autowired private WebApplicationContext wac; private MockMvc mock; @Before public void init() { this.mock = MockMvcBuilders.webAppContextSetup(this.wac).build(); } }
前面的博客我已经整理到了,junit的核心由3个类组成,TestClass,Runner,Suite,注意的是这个suite本身就是一个runner。
认真看过前面的博客,我们就会明白这些类的工作原理,然后我们可以使用junit编写任何需要的测试。
这里我们来自定义运行器来扩展junit。OK,废话不多说了,现在我们开始。
拦截器模式可以看做一种拦截业务方法调用的方法。这种模式通常包含了几个对象,第一个对象就是Interceptor接口,它定义了你的拦截器将实现的一个或多个方法。
第二个对象是Delegate(委托)对象,它持有一系列拦截器。Delegate对象在应用程序的拦截点被调用,并且它会一个接着一个的调用拦截器。
总结一下使用拦截器模式的功能:
1,它带来了可扩展性和灵活性
2,它是的关注分离
3,它增加了可重用性
4,如果没有被正确使用,他可能出现安全问题。
考虑如下一个情节,你正在为一组开发人员设计一个程序。你想让他们尽可能容易的将他们的代码植入你的应用程序中,同时,你又不希望他们改变他。那我们应该要怎么做呢?
你可以在你的应用程序中提供一些点,让那些开发人员能够拦截程序的调用并引入他们自己的逻辑。而其他人无须改变你的代码,他们可以简单的将他们的代码植入你的框架。
OK,现在让我们开始实现自定义运行器。
1,首先写一个拦截器接口,这个拦截器可以通过我们已有的各种拦截器来实现。
package org.linkinpark.junit.testjunit; /** * @创建作者: LinkinPark * @创建时间: 2016年2月14日 * @功能描述: 自定义拦截器接口。 */ public interface Interceptor { /** * @创建时间: 2016年2月14日 * @相关参数: * @功能描述: 过滤器绕前拦截 */ public void interceptBefore(); /** * @创建时间: 2016年2月14日 * @相关参数: * @功能描述: 过滤器绕后拦截 */ public void interceptAfter(); }关于上面的代码,我们自定义2个接口,然后绕前和绕后插入我们自己定义的拦截内容。注意,这里我们的拦截器不接受任何的参数。实现拦截器最常见的模式就是以上下文对象调用拦截器方法,从而能够使得这些方法可以监控和访问我们的应用程序。这样子就可以让我们的应用程序从我们自定义的拦截器方法中获得一些反馈。就目前我们先简单的写2个无参方法好了。
2,现在我们来写delegate对象。前面的博客我也已经整理到了,junit真正的测试执行是在各种Statement抽象类的实现中。那OK,我们现在就写一个自己的测试块。
package org.linkinpark.junit.testjunit; import java.util.ArrayList; import java.util.List; import org.junit.runners.model.Statement; public class InterceptorStatement extends Statement { private final Statement invoker; private List<Interceptor> interceptors = new ArrayList<>(2); public InterceptorStatement(Statement invoker) { this.invoker = invoker; } @Override public void evaluate() throws Throwable { for (Interceptor interceptor : interceptors) { interceptor.interceptBefore(); } invoker.evaluate(); for (Interceptor interceptor : interceptors) { interceptor.interceptAfter(); } } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } }关于上面的代码,我的意图就是将原来junit中的测试作为invoker参数封装到我们自定义的InterceptorStatement中,接下来然后将我们自定义的这个语句传递给我们自己写的测试运行器。
3,现在让我们来写自定义的拦截器。
package org.linkinpark.junit.testjunit; 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 InterceptorRunner extends BlockJUnit4ClassRunner { public InterceptorRunner(Class<?> klass) throws InitializationError { super(klass); } @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { InterceptorStatement statement = new InterceptorStatement(super.methodInvoker(method, test)); InterceptorClasses annotation = test.getClass().getAnnotation(InterceptorClasses.class); Class<?>[] klass = annotation.values(); try { for (Class<?> klas : klass) { statement.addInterceptor((Interceptor)klas.newInstance()); } } catch (Exception e) { e.printStackTrace(); } return statement; } }关于上面的代码,我们自定义一个测试运行器,该类继承BlockJUnit4ClassRunner类。上面的代码我们使用到了一个注解,这个注解来配合我们将我们自定义的测试拦截器实现类加入到我们前面写的delegate对象中,实现对真正测试的回绕拦截。
OK,下面是该注解的代码:
package org.linkinpark.junit.testjunit; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) public @interface InterceptorClasses { public Class<?>[] values(); }
关于上面的代码,这里就不做多的赘述了。这个注解定义一个values方法,用这个注解属性来保存我们插入的拦截器接口的实现类。
这里我们来写2个实现类好了,一个是日志拦截,一个是时间统计拦截。2个测试类的代码如下:
package org.linkinpark.junit.testjunit; public class LoggingInterceptor implements Interceptor { @Override public void interceptBefore() { System.out.println("interceptBefore()。。。"); } @Override public void interceptAfter() { System.out.println("interceptAfter()。。。"); } }
package org.linkinpark.junit.testjunit; public class TimeInterceptor implements Interceptor { Timer timer = new Timer(); private static class Timer { private long startTime = 1; private long endTime = 0; private void start() { startTime = System.currentTimeMillis(); } private void end() { endTime = System.currentTimeMillis(); } private long time() { return (endTime - startTime)/1000; } } @Override public void interceptBefore() { System.out.println("开始计时()。。。"); timer.start(); } @Override public void interceptAfter() { System.out.println("停止计时()。。。"); timer.end(); System.out.println("总共消耗时间为-->" + timer.time() + "毫微秒。。。"); } }
package org.linkinpark.junit.testjunit; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(InterceptorRunner.class) @InterceptorClasses(values = { LoggingInterceptor.class, TimeInterceptor.class }) public class LinkinTest { @Before public void setUp() { System.out.println("setUp()。。。"); } @After public void tearDown() { System.out.println("tearDown()。。。"); } @Test public void test() throws Exception { Thread.sleep(1000); System.out.println("这里开始自己的测试。。。"); Assert.assertEquals("1", "1"); } }运行下看控制台和junit窗口。
setUp()。。。 interceptBefore()。。。 开始计时()。。。 这里开始自己的测试。。。 interceptAfter()。。。 停止计时()。。。 总共消耗时间为-->1毫微秒。。。 tearDown()。。。
OK,最后这里贴出spring中对junit的扩展来结束这篇博客,关于spring的测试执行器SpringJUnit4ClassRunner以后我整理到spring的源码的时候在仔细整理好了。
/* * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.test.context.junit4; import java.lang.reflect.Method; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.Ignore; import org.junit.Test; import org.junit.internal.AssumptionViolatedException; import org.junit.internal.runners.model.EachTestNotifier; import org.junit.internal.runners.model.ReflectiveCallable; import org.junit.internal.runners.statements.ExpectException; import org.junit.internal.runners.statements.Fail; import org.junit.internal.runners.statements.FailOnTimeout; import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.springframework.test.annotation.ExpectedException; import org.springframework.test.annotation.ProfileValueUtils; import org.springframework.test.annotation.Repeat; import org.springframework.test.annotation.Timed; import org.springframework.test.context.TestContextManager; import org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks; import org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks; import org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks; import org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks; import org.springframework.test.context.junit4.statements.SpringFailOnTimeout; import org.springframework.test.context.junit4.statements.SpringRepeat; import org.springframework.util.ReflectionUtils; /** * <p> * {@code SpringJUnit4ClassRunner} is a custom extension of * {@link BlockJUnit4ClassRunner} which provides functionality of the * <em>Spring TestContext Framework</em> to standard JUnit 4.5+ tests by means * of the {@link TestContextManager} and associated support classes and * annotations. * </p> * <p> * The following list constitutes all annotations currently supported directly * by {@code SpringJUnit4ClassRunner}. * <em>(Note that additional annotations may be supported by various * {@link org.springframework.test.context.TestExecutionListener * TestExecutionListeners})</em> * </p> * <ul> * <li>{@link Test#expected() @Test(expected=...)}</li> * <li>{@link ExpectedException @ExpectedException}</li> * <li>{@link Test#timeout() @Test(timeout=...)}</li> * <li>{@link Timed @Timed}</li> * <li>{@link Repeat @Repeat}</li> * <li>{@link Ignore @Ignore}</li> * <li> * {@link org.springframework.test.annotation.ProfileValueSourceConfiguration * @ProfileValueSourceConfiguration}</li> * <li>{@link org.springframework.test.annotation.IfProfileValue * @IfProfileValue}</li> * </ul> * <p> * <b>NOTE:</b> As of Spring 3.0, {@code SpringJUnit4ClassRunner} requires * JUnit 4.5+. * </p> * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 * @see TestContextManager */ @SuppressWarnings("deprecation") public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { private static final Log logger = LogFactory.getLog(SpringJUnit4ClassRunner.class); private final TestContextManager testContextManager; /** * Constructs a new {@code SpringJUnit4ClassRunner} and initializes a * {@link TestContextManager} to provide Spring testing functionality to * standard JUnit tests. * @param clazz the test class to be run * @see #createTestContextManager(Class) */ public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError { super(clazz); if (logger.isDebugEnabled()) { logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "]."); } this.testContextManager = createTestContextManager(clazz); } /** * Creates a new {@link TestContextManager} for the supplied test class and * the configured <em>default {@code ContextLoader} class name</em>. * Can be overridden by subclasses. * @param clazz the test class to be managed * @see #getDefaultContextLoaderClassName(Class) */ protected TestContextManager createTestContextManager(Class<?> clazz) { return new TestContextManager(clazz, getDefaultContextLoaderClassName(clazz)); } /** * Get the {@link TestContextManager} associated with this runner. */ protected final TestContextManager getTestContextManager() { return this.testContextManager; } /** * Get the name of the default {@code ContextLoader} class to use for * the supplied test class. The named class will be used if the test class * does not explicitly declare a {@code ContextLoader} class via the * {@code @ContextConfiguration} annotation. * <p>The default implementation returns {@code null}, thus implying use * of the <em>standard</em> default {@code ContextLoader} class name. * Can be overridden by subclasses. * @param clazz the test class * @return {@code null} */ protected String getDefaultContextLoaderClassName(Class<?> clazz) { return null; } /** * Returns a description suitable for an ignored test class if the test is * disabled via {@code @IfProfileValue} at the class-level, and * otherwise delegates to the parent implementation. * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class) */ @Override public Description getDescription() { if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) { return Description.createSuiteDescription(getTestClass().getJavaClass()); } return super.getDescription(); } /** * Check whether the test is enabled in the first place. This prevents * classes with a non-matching {@code @IfProfileValue} annotation * from running altogether, even skipping the execution of * {@code prepareTestInstance()} {@code TestExecutionListener} * methods. * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class) * @see org.springframework.test.annotation.IfProfileValue * @see org.springframework.test.context.TestExecutionListener */ @Override public void run(RunNotifier notifier) { if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) { notifier.fireTestIgnored(getDescription()); return; } super.run(notifier); } /** * Wraps the {@link Statement} returned by the parent implementation with a * {@link RunBeforeTestClassCallbacks} statement, thus preserving the * default functionality but adding support for the Spring TestContext * Framework. * @see RunBeforeTestClassCallbacks */ @Override protected Statement withBeforeClasses(Statement statement) { Statement junitBeforeClasses = super.withBeforeClasses(statement); return new RunBeforeTestClassCallbacks(junitBeforeClasses, getTestContextManager()); } /** * Wraps the {@link Statement} returned by the parent implementation with a * {@link RunAfterTestClassCallbacks} statement, thus preserving the default * functionality but adding support for the Spring TestContext Framework. * @see RunAfterTestClassCallbacks */ @Override protected Statement withAfterClasses(Statement statement) { Statement junitAfterClasses = super.withAfterClasses(statement); return new RunAfterTestClassCallbacks(junitAfterClasses, getTestContextManager()); } /** * Delegates to the parent implementation for creating the test instance and * then allows the {@link #getTestContextManager() TestContextManager} to * prepare the test instance before returning it. * @see TestContextManager#prepareTestInstance(Object) */ @Override protected Object createTest() throws Exception { Object testInstance = super.createTest(); getTestContextManager().prepareTestInstance(testInstance); return testInstance; } /** * Performs the same logic as * {@link BlockJUnit4ClassRunner#runChild(FrameworkMethod, RunNotifier)}, * except that tests are determined to be <em>ignored</em> by * {@link #isTestMethodIgnored(FrameworkMethod)}. */ @Override protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) { EachTestNotifier eachNotifier = springMakeNotifier(frameworkMethod, notifier); if (isTestMethodIgnored(frameworkMethod)) { eachNotifier.fireTestIgnored(); return; } eachNotifier.fireTestStarted(); try { methodBlock(frameworkMethod).evaluate(); } catch (AssumptionViolatedException e) { eachNotifier.addFailedAssumption(e); } catch (Throwable e) { eachNotifier.addFailure(e); } finally { eachNotifier.fireTestFinished(); } } /** * {@code springMakeNotifier()} is an exact copy of * {@link BlockJUnit4ClassRunner BlockJUnit4ClassRunner's} * {@code makeNotifier()} method, but we have decided to prefix it with * "spring" and keep it {@code private} in order to avoid the * compatibility clashes that were introduced in JUnit between versions 4.5, * 4.6, and 4.7. */ private EachTestNotifier springMakeNotifier(FrameworkMethod method, RunNotifier notifier) { Description description = describeChild(method); return new EachTestNotifier(notifier, description); } /** * Augments the default JUnit behavior * {@link #withPotentialRepeat(FrameworkMethod, Object, Statement) with * potential repeats} of the entire execution chain. * <p>Furthermore, support for timeouts has been moved down the execution chain * in order to include execution of {@link org.junit.Before @Before} * and {@link org.junit.After @After} methods within the timed * execution. Note that this differs from the default JUnit behavior of * executing {@code @Before} and {@code @After} methods * in the main thread while executing the actual test method in a separate * thread. Thus, the end effect is that {@code @Before} and * {@code @After} methods will be executed in the same thread as * the test method. As a consequence, JUnit-specified timeouts will work * fine in combination with Spring transactions. Note that JUnit-specific * timeouts still differ from Spring-specific timeouts in that the former * execute in a separate thread while the latter simply execute in the main * thread (like regular tests). * @see #possiblyExpectingExceptions(FrameworkMethod, Object, Statement) * @see #withBefores(FrameworkMethod, Object, Statement) * @see #withAfters(FrameworkMethod, Object, Statement) * @see #withPotentialTimeout(FrameworkMethod, Object, Statement) * @see #withPotentialRepeat(FrameworkMethod, Object, Statement) */ @Override protected Statement methodBlock(FrameworkMethod frameworkMethod) { Object testInstance; try { testInstance = new ReflectiveCallable() { @Override protected Object runReflectiveCall() throws Throwable { return createTest(); } }.run(); } catch (Throwable ex) { return new Fail(ex); } Statement statement = methodInvoker(frameworkMethod, testInstance); statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement); statement = withBefores(frameworkMethod, testInstance, statement); statement = withAfters(frameworkMethod, testInstance, statement); statement = withRulesReflectively(frameworkMethod, testInstance, statement); statement = withPotentialRepeat(frameworkMethod, testInstance, statement); statement = withPotentialTimeout(frameworkMethod, testInstance, statement); return statement; } /** * Invokes JUnit 4.7's private {@code withRules()} method using * reflection. This is necessary for backwards compatibility with the JUnit * 4.5 and 4.6 implementations of {@link BlockJUnit4ClassRunner}. */ private Statement withRulesReflectively(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { Method withRulesMethod = ReflectionUtils.findMethod(getClass(), "withRules", FrameworkMethod.class, Object.class, Statement.class); if (withRulesMethod != null) { // Original JUnit 4.7 code: // statement = withRules(frameworkMethod, testInstance, statement); ReflectionUtils.makeAccessible(withRulesMethod); statement = (Statement) ReflectionUtils.invokeMethod(withRulesMethod, this, frameworkMethod, testInstance, statement); } return statement; } /** * Returns {@code true} if {@link Ignore @Ignore} is present for * the supplied {@link FrameworkMethod test method} or if the test method is * disabled via {@code @IfProfileValue}. * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Method, Class) */ protected boolean isTestMethodIgnored(FrameworkMethod frameworkMethod) { Method method = frameworkMethod.getMethod(); return (method.isAnnotationPresent(Ignore.class) || !ProfileValueUtils.isTestEnabledInThisEnvironment(method, getTestClass().getJavaClass())); } /** * Performs the same logic as * {@link BlockJUnit4ClassRunner#possiblyExpectingExceptions(FrameworkMethod, Object, Statement)} * except that the <em>expected exception</em> is retrieved using * {@link #getExpectedException(FrameworkMethod)}. */ @Override protected Statement possiblyExpectingExceptions(FrameworkMethod frameworkMethod, Object testInstance, Statement next) { Class<? extends Throwable> expectedException = getExpectedException(frameworkMethod); return expectedException != null ? new ExpectException(next, expectedException) : next; } /** * Get the {@code exception} that the supplied {@link FrameworkMethod * test method} is expected to throw. * <p>Supports both Spring's {@link ExpectedException @ExpectedException(...)} * and JUnit's {@link Test#expected() @Test(expected=...)} annotations, but * not both simultaneously. * @return the expected exception, or {@code null} if none was specified */ protected Class<? extends Throwable> getExpectedException(FrameworkMethod frameworkMethod) { Test testAnnotation = frameworkMethod.getAnnotation(Test.class); Class<? extends Throwable> junitExpectedException = (testAnnotation != null && testAnnotation.expected() != Test.None.class ? testAnnotation.expected() : null); ExpectedException expectedExAnn = frameworkMethod.getAnnotation(ExpectedException.class); Class<? extends Throwable> springExpectedException = (expectedExAnn != null ? expectedExAnn.value() : null); if (springExpectedException != null && junitExpectedException != null) { String msg = "Test method [" + frameworkMethod.getMethod() + "] has been configured with Spring's @ExpectedException(" + springExpectedException.getName() + ".class) and JUnit's @Test(expected=" + junitExpectedException.getName() + ".class) annotations. " + "Only one declaration of an 'expected exception' is permitted per test method."; logger.error(msg); throw new IllegalStateException(msg); } return springExpectedException != null ? springExpectedException : junitExpectedException; } /** * Supports both Spring's {@link Timed @Timed} and JUnit's * {@link Test#timeout() @Test(timeout=...)} annotations, but not both * simultaneously. Returns either a {@link SpringFailOnTimeout}, a * {@link FailOnTimeout}, or the unmodified, supplied {@link Statement} as * appropriate. * @see #getSpringTimeout(FrameworkMethod) * @see #getJUnitTimeout(FrameworkMethod) */ @Override protected Statement withPotentialTimeout(FrameworkMethod frameworkMethod, Object testInstance, Statement next) { Statement statement = null; long springTimeout = getSpringTimeout(frameworkMethod); long junitTimeout = getJUnitTimeout(frameworkMethod); if (springTimeout > 0 && junitTimeout > 0) { String msg = "Test method [" + frameworkMethod.getMethod() + "] has been configured with Spring's @Timed(millis=" + springTimeout + ") and JUnit's @Test(timeout=" + junitTimeout + ") annotations. Only one declaration of a 'timeout' is permitted per test method."; logger.error(msg); throw new IllegalStateException(msg); } else if (springTimeout > 0) { statement = new SpringFailOnTimeout(next, springTimeout); } else if (junitTimeout > 0) { statement = new FailOnTimeout(next, junitTimeout); } else { statement = next; } return statement; } /** * Retrieves the configured JUnit {@code timeout} from the {@link Test * @Test} annotation on the supplied {@link FrameworkMethod test method}. * @return the timeout, or {@code 0} if none was specified. */ protected long getJUnitTimeout(FrameworkMethod frameworkMethod) { Test testAnnotation = frameworkMethod.getAnnotation(Test.class); return (testAnnotation != null && testAnnotation.timeout() > 0 ? testAnnotation.timeout() : 0); } /** * Retrieves the configured Spring-specific {@code timeout} from the * {@link Timed @Timed} annotation on the supplied * {@link FrameworkMethod test method}. * @return the timeout, or {@code 0} if none was specified. */ protected long getSpringTimeout(FrameworkMethod frameworkMethod) { Timed timedAnnotation = frameworkMethod.getAnnotation(Timed.class); return (timedAnnotation != null && timedAnnotation.millis() > 0 ? timedAnnotation.millis() : 0); } /** * Wraps the {@link Statement} returned by the parent implementation with a * {@link RunBeforeTestMethodCallbacks} statement, thus preserving the * default functionality but adding support for the Spring TestContext * Framework. * @see RunBeforeTestMethodCallbacks */ @Override protected Statement withBefores(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { Statement junitBefores = super.withBefores(frameworkMethod, testInstance, statement); return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(), getTestContextManager()); } /** * Wraps the {@link Statement} returned by the parent implementation with a * {@link RunAfterTestMethodCallbacks} statement, thus preserving the * default functionality but adding support for the Spring TestContext * Framework. * @see RunAfterTestMethodCallbacks */ @Override protected Statement withAfters(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { Statement junitAfters = super.withAfters(frameworkMethod, testInstance, statement); return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(), getTestContextManager()); } /** * Supports Spring's {@link Repeat @Repeat} annotation by returning a * {@link SpringRepeat} statement initialized with the configured repeat * count or {@code 1} if no repeat count is configured. * @see SpringRepeat */ protected Statement withPotentialRepeat(FrameworkMethod frameworkMethod, Object testInstance, Statement next) { Repeat repeatAnnotation = frameworkMethod.getAnnotation(Repeat.class); int repeat = (repeatAnnotation != null ? repeatAnnotation.value() : 1); return new SpringRepeat(next, frameworkMethod.getMethod(), repeat); } }