对我来说,这仍然感觉像是 Spring Framework 测试框架中的一个错误,而不是 Spring Boot。javadoc 指出prepareTestInstance
它“应该在测试实例化后立即调用”。使用时这并不成立,@Repeat
这感觉就像是一个框架错误。
Javadoc 声明@Repeat
如下:
请注意,要重复执行的范围包括测试方法本身的执行以及测试装置的任何设置或拆除。
这并不表示要创建测试类的新实例。它也不表示每次重复时都会重新准备测试实例。
这与 的实施是一致的SpringJUnit4ClassRunner
。
然而,为了支持prepareTestInstance
Spring 的 JUnit 4 规则,SpringMethodRule
确实需要RunPrepareTestInstanceCallbacks
为每次重复准备测试实例,如堆栈跟踪中所见。
[2021-08-20 16:39:21.772] - 79836 SEVERE [pool-1-thread-3] --- org.springframework.test.context.TestContextManager: Caught exception while allowing TestExecutionListener [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@2a8448fa] to prepare test instance [test.ConcurrentSampleTest@38e98121]
org.springframework.beans.factory.BeanCreationException: Could not inject field: private business_logic.SomeOtherBean test.ConcurrentSampleTest.someOtherBean; nested exception is java.lang.IllegalStateException: The field private business_logic.SomeOtherBean test.ConcurrentSampleTest.someOtherBean cannot have an existing value
at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.inject(MockitoPostProcessor.java:364)
at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.inject(MockitoPostProcessor.java:352)
at org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener.lambda$injectFields$0(MockitoTestExecutionListener.java:94)
at org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener.postProcessFields(MockitoTestExecutionListener.java:115)
at org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener.injectFields(MockitoTestExecutionListener.java:94)
at org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener.prepareTestInstance(MockitoTestExecutionListener.java:61)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:244)
at org.springframework.test.context.junit4.statements.RunPrepareTestInstanceCallbacks.evaluate(RunPrepareTestInstanceCallbacks.java:63)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.springframework.test.context.junit4.statements.SpringFailOnTimeout.evaluate(SpringFailOnTimeout.java:87)
at org.springframework.test.context.junit4.statements.ProfileValueChecker.evaluate(ProfileValueChecker.java:103)
at ice.bricks.exceptions.ExceptionUtils.runSafe(ExceptionUtils.java:34)
at test_utils.ConcurrentTestStatement.lambda$1(ConcurrentTestStatement.java:32)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.IllegalStateException: The field private business_logic.SomeOtherBean test.ConcurrentSampleTest.someOtherBean cannot have an existing value
at org.springframework.util.Assert.state(Assert.java:97)
at org.springframework.boot.test.mock.mockito.MockitoPostProcessor.inject(MockitoPostProcessor.java:358)
... 17 more
记录显示,JUnit 5 不会出现同样的问题,在 JUnit 5 中,@RepeatedTest
每次调用重复测试都会创建一个新的测试类实例。
这是正确的,并且这指出了一个非常关键的区别。
扩展SpringJUnit4ClassRunner
了 JUnit 4,BlockJUnit4ClassRunner
它根本不是为测试方法的并发调用而设计的;而 JUnit Jupiter 则@RepeatedTest
是为此类用例而设计的。
例如,以下基于 JUnit Jupiter 的测试类并发执行重复,并且全部通过。
为了启用并发执行,您需要设置 JVM 系统属性:-Djunit.jupiter.execution.parallel.enabled=true
。
@SpringBootTest(classes = { SomeBean.class, SomeOtherBean.class })
class ConcurrentSampleJupiterTests {
@MockBean
private SomeBean someBean;
@MockBean
private SomeOtherBean someOtherBean;
@BeforeEach
void configureMock() {
when(this.someBean.getData()).thenReturn("some mocked data");
System.err.printf(">> Thread: %s --> hashes for someBean/someOtherBean's:\t%d/%d%n",
Thread.currentThread().getName(), System.identityHashCode(this.someBean),
System.identityHashCode(this.someOtherBean));
}
@RepeatedTest(10)
@Execution(ExecutionMode.CONCURRENT)
void concurrentTest() {
assertEquals(this.someBean.getData(), "some mocked data");
}
}