Learn to test spring boot async rest controller using MockMVC which support async request processing and return the response asynchronously.
1. spring boot async controller
Given is a async controller which returns a simple string output after the delay of 5 seconds. We will write the unit test for this controller.
import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadLocalRandom; import org.springframework.core.task.TaskExecutor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloWorldCompletableFutureController { private final TaskExecutor taskExecutor; public HelloWorldCompletableFutureController(TaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @GetMapping(value = "/testCompletableFuture") public CompletableFuture<String> echoHelloWorld2() { return CompletableFuture.supplyAsync(() -> { randomDelay(); return "Hello World !!"; }, taskExecutor); } private void randomDelay() { try { Thread.sleep(ThreadLocalRandom.current().nextInt(5000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
2. How to test async rest controller with MockMvc
Just as with regular controllers, the Spring WebMVC Test framework can be used to test async controllers.
Create a test class and annotate it with @WebMvcTest and @RunWith(SpringRunner.class). The @WebMvcTest
will bootstrap a minimal Spring Boot application containing the things needed to test the controller. It automatically configures Spring MockMvc, which is autowired into the test.
Please note that to test a async controller, the async dispatching needs to be initiated using asyncDispatch() method.
Below given test perform following things:
- The initial request is performed and validated for async feature to be started.
- The
asyncDispatch()
is applied and request is sent. - Finally, we assert the expected response.
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import com.howtodoinjava.springasyncexample.web.controller.HelloWorldCompletableFutureController; @RunWith(SpringRunner.class) @WebMvcTest(HelloWorldCompletableFutureController.class) public class HelloWorldCompletableFutureControllerTest { @Autowired private MockMvc mockMvc; @Test public void testHelloWorldController() throws Exception { MvcResult mvcResult = mockMvc.perform(get("/testCompletableFuture")) .andExpect(request().asyncStarted()) .andDo(MockMvcResultHandlers.log()) .andReturn(); mockMvc.perform(asyncDispatch(mvcResult)) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith("text/plain")) .andExpect(content().string("Hello World !!")); } }
3. Sourcecode files
The application class which bootstrap the application and configure the async behavior. To override the default async behavior such as thread pool and timeout, you can implement the WebMvcConfigurer
interface and override it’s configureAsyncSupport() method.
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @SpringBootApplication public class SpringAsyncExampleApplication implements WebMvcConfigurer { public static void main(String[] args) { SpringApplication.run(SpringAsyncExampleApplication.class, args); } @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { configurer.setTaskExecutor(mvcTaskExecutor()); configurer.setDefaultTimeout(30_000); } @Bean public ThreadPoolTaskExecutor mvcTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setThreadNamePrefix("mvc-task-"); return taskExecutor; } }
3.1. SpringAsyncExampleApplication.java
3.2. application.properties
Enable debug logging here to understand the behavior of the application.
logging.level.org.springframework=DEBUG logging.level.com.howtodoinjava=DEBUG
3.3. pom.xml
The used pom.xml is:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.howtodoinjava.demo</groupId> <artifactId>spring-async-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-async-demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>repository.spring.release</id> <name>Spring GA Repository</name> <url>http://repo.spring.io/release</url> </repository> </repositories> </project>
Let me know if you face any error while testing the asynchronous spring boot rest controller.
Happy Learning !!