Search code examples
spring-boot-testvirtual-threads

How to enable virtual threads in springBootTest


Is there a way to enable virtual threads when executing through @SpringBootTest? I've tried both known ways, but nothing works with @SpringBootTest. 1: spring.threads.virtual.enabled=true 2: with beans

Here are my tests:

  1. my controller:
    @GetMapping("/api")
    protected ResponseEntity<?> runningOnVirtualThread() {
        System.out.println(Thread.currentThread());
        return ResponseEntity
                .ok().body(Thread.currentThread().isVirtual());
    }
  1. tests
    @Autowired
    protected MockMvc mockMvc;

    @Test
    void testing_with_MockMvc() throws Exception {//DO NOT PASS
        var response = mockMvc.perform(MockMvcRequestBuilders.get("/api")).andExpect(status().isOk()).andReturn();
        //controller returns false, so the next fails.
        Assertions.assertEquals("true",response.getResponse().getContentAsString());
    }

    @Test
    void testing_with_RestAssured() {//PASS
        DemoApplication.main(new String[]{});
        RestAssured.given().when().get("http://localhost:8080/api").then().statusCode(200).and().body(equalTo("true"));
    }

Solution

  • To begin with, testing your endpoint with RestAssured-powered full-blown HTTP request to it and with Mock Mvc technique are two completely different things from the standpoint of the execution of this endpoint.

    RestAssured, or just plain TestRestTemplate, issues an HTTP request that does hit the (embedded) Tomcat server and whatever you configured for Tomcat Connector, protocol, with Spring Boot property spring.threads.virtual.enabled etc gets invoked and comes into play.

    In contrary, Mock Mvc technique completely bypasses the Tomcat Connectors and other Tomcat machineries, instead handing the HTTP request directly to Spring TestDispatcherServlet.

    Therefore, with Mock Mvc, your Controller method executes on the same thread as your test executes, while with REST techniques this method executes on Tomcat Connector-controlled thread.

    Therefore, the situation you stumbled across when with Mock Mvc the Controller method is not executed on virtual thread is absolutely normal and I wouldn't bother about it at all.

    Although I see little to no value whatsoever in "fixing" this issue and in forcing Mock Mvc to run on a virtual thread, it is difficult to envision all the ramifications you have with your tests, and, if it is absolutely necessary, then I could offer a very simple walkaround - to run your Mock Mvc test already on a virtual thread.

    Technically, it might look like a drop-in replacement of MockMvc class, let's call it MockMvcOnVirtualThreads; you can pick up less awkward name.

    public class MockMvcOnVirtualThreads {
        
        private final MockMvc delegatee;
        private final ExecutorService executor;
    
        public MockMvcOnVirtualThreads(MockMvc delegatee) {
            this.delegatee = delegatee;
            this.executor = Executors.newVirtualThreadPerTaskExecutor();
        }
    
        public DispatcherServlet getDispatcherServlet() {
            return delegatee.getDispatcherServlet();
        }
        
        public ResultActions perform(RequestBuilder requestBuilder) throws Exception {
            return executor.submit(() -> delegatee.perform(requestBuilder)).get();
        }
        
    }
    

    It then could be instantiated like this:

    @Configuration
    public class MockMvcConfigurer {
        
        @Autowired
        private MockMvc mockMvc;
        
        @Bean 
        public MockMvcOnVirtualThreads mockMvcOnVirtualThreads() {
            return new MockMvcOnVirtualThreads(mockMvc);
        }
        
    }
    

    and then, as a drop-in, you just inject in your tests this MockMvcOnVirtualThreads instance instead of original, genuine MockMvc:

    @Autowired
    protected MockMvcOnVirtualThreads mockMvc;
    
    @Test
    void testing_with_MockMvc() throws Exception {
        mockMvc.perform...
    }
    

    (Don't forget to let Spring Boot Test engine to pick up MockMvcConfigurer)

    As you can see, the above differs very little from plain execution of your test on a virtual thread like this:

        Thread.ofVirtual().start(() -> {
            try {
                ResultActions response = mockMvc.perform(MockMvcRequestBuilders.get("/api"));
            } catch (Exception e) {
                ...
            }
        }).join();
    

    , just very basic wrapping.

    If everything above makes at least some sense to you, it would be easy for me to publish fully working POC, please let me know.

    Few additional in-depth notes:

    • It is easy to see that MockMvc, TestDispatcherServlet and others up-stack of your tests ignore Spring Boot property spring.threads.virtual.enabled, while they could honor it. Whether Spring men would do in the future and honor virtual thread setting in MockMvc is difficult for me to say.

    • Invocation on a virtual thread with Mock Mvc could be done upper-in-stack on other Spring layers: TestDispatcherServlet/DispatcherServlet, HandlerAdapter, (moreover RequestMappingHandlerAdapter has settable TaskExecutor), interceptors and so on. At this point of time I cannot speak of advantages or disadvantages of such tweaking.

    • MockMvc, TestDispatcherServlet, which is just a subclass of DispatcherServlet, also supports asynchronous requests. I was not able to effectively use this technique for the purpose of executing on a virtual thread and cannot say at the moment might this be a right way to go.