Skip to content

Support parallel test execution with @AutoConfigureMockMvc #16179

@gandrade

Description

@gandrade

I've created a small project running atop of Spring Boot 2.2.0.BUILD-SNAPSHOT version and using JUnit 5 as the test runner. There, running a single-test available, repeated times, using @Repeated annotation, and in parallel, I've been facing the following stack trace, when an assertion fails because it took more than 100ms to respond back.
Of course, the point is not about the sample code, it's about how SpringBootMockMvcBuilderCustomizer class handles to writing those lines down.

Would make sense change List collection for a thread-safe approach?

SpringBootMockMvcBuilderCustomizer.java

 private static class SystemLinesWriter implements SpringBootMockMvcBuilderCustomizer.LinesWriter {
        private final MockMvcPrint print;

        SystemLinesWriter(MockMvcPrint print) {
            this.print = print;
        }

        public void write(List<String> lines) {
            PrintStream printStream = this.getPrintStream();
            Iterator var3 = lines.iterator();

            while(var3.hasNext()) {
                String line = (String)var3.next();
                printStream.println(line);
            }

        }

        private PrintStream getPrintStream() {
            return this.print == MockMvcPrint.SYSTEM_ERR ? System.err : System.out;
        }
    }

Sample test code

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class DummyApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @DisplayName("Should return transaction statistics respecting a response-time threshold even when a concurrent random traffic incomes")
    @RepeatedTest(10000)
    public void dummy() throws Exception {
       TransactionDTO transactionDTO = new TransactionDTO(
                (double) new Random().nextInt(100000),
                OffsetDateTime.now().toEpochSecond() + new Random().nextInt(120));

        this.mockMvc.perform(post("/transactions")
                .content(objectMapper.writeValueAsString(transactionDTO))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isCreated());

        assertTimeoutPreemptively(ofMillis(100), () -> {
            this.mockMvc.perform(get("/statistics")
                    .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());
        });
    }
}

junit-application.properties

junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent

Stack trace

org.opentest4j.AssertionFailedError: execution timed out after 100 ms

	at org.junit.jupiter.api.AssertTimeout.assertTimeoutPreemptively(AssertTimeout.java:144)
	at org.junit.jupiter.api.AssertTimeout.assertTimeoutPreemptively(AssertTimeout.java:115)
	at org.junit.jupiter.api.AssertTimeout.assertTimeoutPreemptively(AssertTimeout.java:97)
	at org.junit.jupiter.api.AssertTimeout.assertTimeoutPreemptively(AssertTimeout.java:93)
	at org.junit.jupiter.api.Assertions.assertTimeoutPreemptively(Assertions.java:3236)
	at io.github.gandrade.n26.N26ApplicationTests.contextLoads(N26ApplicationTests.java:49)
	at sun.reflect.GeneratedMethodAccessor35.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:532)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:171)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:167)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:114)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:59)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:108)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
	at org.junit.platform.engine.support.hierarchical.ForkJoinPoolHierarchicalTestExecutorService$ExclusiveTask.compute(ForkJoinPoolHierarchicalTestExecutorService.java:170)
	at java.util.concurrent.RecursiveAction.exec(RecursiveAction.java:189)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
	Suppressed: java.util.ConcurrentModificationException
		at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
		at java.util.ArrayList$Itr.next(ArrayList.java:859)
		at org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer$SystemLinesWriter.write(SpringBootMockMvcBuilderCustomizer.java:296)
		at org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer$DeferredLinesWriter.writeDeferredResult(SpringBootMockMvcBuilderCustomizer.java:246)
		at org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener.afterTestMethod(MockMvcPrintOnlyOnFailureTestExecutionListener.java:44)
		at org.springframework.test.context.TestContextManager.afterTestMethod(TestContextManager.java:443)
		at org.springframework.test.context.junit.jupiter.SpringExtension.afterEach(SpringExtension.java:139)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAfterEachCallbacks$11(TestMethodTestDescriptor.java:218)
		at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAllAfterMethodsOrCallbacks$13(TestMethodTestDescriptor.java:230)
		at java.util.ArrayList.forEach(ArrayList.java:1257)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAllAfterMethodsOrCallbacks(TestMethodTestDescriptor.java:228)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAfterEachCallbacks(TestMethodTestDescriptor.java:217)
		at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:120)
		... 11 more

Workaround

To suppress this behavior, I've added the following configuration for @AutoConfigureMockMvc annotation:
@AutoConfigureMockMvc(print = MockMvcPrint.NONE), which AFAIK, doesn't make the code traverse the List.

After this change, placebo or not, I got the impression of a lower number of "false-positives" timeout assertions.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions