如何使用 spring boot 和 mockito 对控制器方法进行单元测试

How to unit test a controller method with spring boot and mockito

我已经阅读了几篇文章并观看了一些视频,虽然我掌握了 Mockito 的工作原理,但我仍然无法对我的控制器实施任何测试。

首先,我有以下服务 class,它向外部 API 执行请求:

@Service
public class MovieService {

    RestTemplate restTemplate = new RestTemplate();
    
    public MovieResponse getMovieResponse() {
        ResponseEntity<MovieResponse> responseEntity = restTemplate.
                getForEntity("{base_url}", MovieResponse.class);
        return responseEntity.getBody();
    }

}

然后我的controller调用这个方法对数据进行排序操作:

@RestController
public class MovieController {
    
    @Autowired
    MovieService movieService = new MovieService();

    @GetMapping(value="/movies")
    public List<Movie> getAllMovies() {
        List<Movie> movies = movieService.getMovieResponse().getMovies();
        movies.sort(Comparator.comparing(Movie::getMovieName));
        return movies ;
    
    }
}

所以我尝试模拟服务响应以测试我的方法并验证排序是否正在进行。

@RunWith(MockitoJUnitRunner.class)
class MovieControllerTest{
    
    private MovieService movieService;
    
    @Autowired
    private MockMvc mockMvc;
    
    @Before
    void setup() {
        movieService = Mockito.mock(MovieService.class);
    }
    
    
    @Test
    void getAllMoviesTest() throws Exception {
        Movie movie1 = new Movie("Marley & Me", "2008");
        Movie movie2 = new Movie("Avatar", "2012");

        List<Movie> allMovies = Arrays.asList(movie2, movie1);
        List<Movie> expectedMovies = Arrays.asList(movie1, movie2);
        
        MovieResponse firstMovieResponse = new MovieResponse();
        firstMovieResponse.setMovies(allMovies);
        
        MovieResponse expectedMovieResponse = new MovieResponse();
        firstMovieResponse.setMovies(expectedMovies);
        
        Mockito.when(movieService.getMovieResponse())
                .thenReturn(firstMovieResponse);
        
        mockMvc.perform(get("/movies"))
                .andExpect(status().isOk())
                .andExpect((ResultMatcher) expectedMovieResponse);
  }
}

我尝试了几种方法,但我认为这是我所拥有的最接近的方法,但我在行中得到了 NullPointerException

Mockito.when(movieService.getMovieResponse())
                    .thenReturn(firstMovieResponse);

并且堆栈跟踪打印以下内容:

java.lang.NullPointerException
    at com.example.movieservice.controller.MovieControllerTest.getAllMoviesTest(MovieControllerTest.java:50)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod[=15=](ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke[=15=](ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod(TestMethodTestDescriptor.java:210)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute[=15=](EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:84)
    at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:98)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:768)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:464)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)

您需要在@Before注解的设置函数中的getAllMoviesTest中进行设置。您还需要在测试中使用 @MockBean 注释 movieService class 但正如之前有人所说,查看堆栈跟踪以找出发生 NullPointerException 的位置。

我认为 NPE 即将到来,因为 movieservice 没有被模拟并且 movieService 对象可能为空,所以它可能会产生 NPE。尝试将@MockBean 放在移动服务之上。 尝试 运行 在调试模式下进行测试。

如果您的目标是控制排名,这可能会派上用场。

@RunWith(MockitoJUnitRunner.class)
class MovieControllerTest{
    
    @InjectMocks
    private MovieController movieController;

    @Mock
    private MovieService movieService;
    
    @Test
    void getAllMoviesTest() throws Exception {
        //Given
        Movie movie1 = new Movie("Marley & Me", "2008");
        Movie movie2 = new Movie("Avatar", "2012");
        final List<Movie> allMovies = Arrays.asList(movie2, movie1);
        
        MovieResponse firstMovieResponse = new MovieResponse();
        firstMovieResponse.setMovies(allMovies);
        
        Mockito.when(movieService.getMovieResponse()).thenReturn(firstMovieResponse);
        
        //When
        List<Movie> actualMovieList = movieController.getAllMovies();

        //Then
        assertThat(actualMovieList).containsExactly(movie1, movie2);
  }
}

@InjectMocks,将模拟 class 注入测试 class.

PS。您还可以使用不同的东西来测试排序。

assertThat(actualMovieList).containsExactly(movie1, movie2);

通常单元测试意味着直接调用主题 class 并存根其依赖项。提供的代码示例依赖于 @Autowired 注释,需要存在 Spring 上下文才能工作。

测试用例尝试调用不可用的 mockMvc,因此抛出 NullPointerException

您必须使用 @SpringBootTest 显式注释测试 class,以便创建上下文。此外,必须包含 @AutoConfigureMockMvc 以便 MockMvc 可用于自动装配。

最后但同样重要的是,由于我们经常使用 Spring 的上下文,因此我们必须用 Spring 的 @MockBean 包装它,而不是使用普通的 Mockito ,这会将存根注入控制器。

@SpringBootTest
@AutoConfigureMockMvc
class MovieControllerTest {

    @MockBean
    private MovieService movieService;

    @Autowired
    private MockMvc mockMvc;

    @Test
    void getAllMoviesTest() throws Exception {
        // TODO: when(movieService...).thenReturn(...);
        // TODO: mockMvc.perform(...);
    }
}

请参阅 Spring's documentation 了解更多信息。