Spring MVC是用于构建企业Java应用程序的最受欢迎的Java框架之一,非常适合测试。 通过设计,Spring MVC促进了关注点的分离并鼓励针对接口进行编码。 这些特性以及Spring对依赖项注入的实现,使Spring应用程序非常容易测试。

本教程是我使用JUnit 5进行单元测试的第二部分。 我将向您展示如何将JUnit 5与Spring集成,然后向您介绍可用于测试Spring MVC控制器,服务和存储库的三个工具。

下载
下载本教程中使用的示例应用程序的源代码。 由Steven Haines为JavaWorld创建。

将JUnit 5与Spring 5集成

对于本教程,我们将使用Maven和Spring Boot,因此我们要做的第一件事是将JUnit 5依赖项添加到我们的Maven POM文件中:


    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.6.0</version>
      <scope>test</scope>
    </dependency>

就像我们在第1部分中所做的一样,在此示例中,我们将使用Mockito。 因此,我们将需要添加JUnit 5 Mockito库:


    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-junit-jupiter</artifactId>
      <version>3.2.4</version>
      <scope>test</scope>
    </dependency>
    

@ExtendWith和SpringExtension类

JUnit 5定义了一个扩展接口 ,通过该接口 ,类可以在执行生命周期的各个阶段与JUnit测试集成。 我们可以通过在测试类中添加@ExtendWith批注并指定要加载的扩展类来启用扩展。 然后,扩展可以实现各种回调接口,这些接口将在整个测试生命周期中被调用:在所有测试运行之前,在每个测试运行之前,在每个测试运行之后以及在所有测试运行之后。

Spring定义了一个SpringExtension类,该类订阅JUnit 5生命周期通知以创建和维护“测试上下文”。 回想一下,Spring的应用程序上下文包含应用程序中的所有Spring Bean,并且它执行依赖项注入以将应用程序及其依赖项组合在一起。 Spring使用JUnit 5扩展模型来维护测试的应用程序上下文,这使得使用Spring编写单元测试变得简单。

在将JUnit 5库添加到Maven POM文件之后,我们可以使用SpringExtension.class扩展我们的JUnit 5测试类:


@ExtendWith(SpringExtension.class)
class MyTests {
  // ...
}

在这种情况下,该示例是一个Spring Boot应用程序。 幸运的是, @SpringBootTest批注已经包含@ExtendWith(SpringExtension.class)批注,因此我们只需要包含@SpringBootTest

添加Mockito依赖项

为了正确地隔离测试每个组件并模拟不同的场景,我们将要为每个类的依赖关系创建模拟实现。 这是Mockito的来源。在您的POM文件中包括以下依赖项,以添加对Mockito的支持:


    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-junit-jupiter</artifactId>
      <version>3.2.4</version>
      <scope>test</scope>
    </dependency>
    

将JUnit 5和Mockito集成到Spring应用程序中之后,您可以通过使用@MockBean批注在测试类中简单定义一个Spring bean(例如服务或存储库)来利用@MockBean 。 这是我们的示例:


@SpringBootTest
public class WidgetServiceTest {
    /**
     * Autowire in the service we want to test
     */
    @Autowired
    private WidgetService service;

    /**
     * Create a mock implementation of the WidgetRepository
     */
    @MockBean
    private WidgetRepository repository;
    ...
}

在此示例中,我们在WidgetServiceTest类中创建了一个模拟WidgetRepository 。 当Spring看到此情况时,它将自动将其连接到我们的WidgetService以便我们可以在测试方法中创建不同的场景。 每种测试方法都将配置WidgetRepository的行为,例如通过返回所请求的Widget或为找不到数据的查询返回Optional.empty() 。 我们将在本教程的其余部分中研究配置这些模拟bean的各种方法的示例。

Spring MVC示例应用程序

要编写基于Spring的单元测试,我们需要一个应用程序来针对它们进行编写。 幸运的是,我们可以使用我的Spring系列教程“ 精通Spring框架5,第1部分:Spring MVC ”中的示例应用程序。 我使用了该教程中的示例应用程序作为基础应用程序。 我使用更强大的REST API对其进行了修改,以便我们还要测试更多内容。

该示例应用程序是一个具有REST控制器,服务层和使用Spring Data JPA来在“内存小工具”与H2内存数据库之间进行持久化的存储库的Spring MVC Web应用程序。 图1是概述。

Spring MVC示例应用程序图。 史蒂文·海恩斯
图1. Spring MVC示例应用程序图

什么是小部件?

Widget只是具有ID,名称,描述和版本号的“事物”。 在这种情况下,我们的小部件将使用JPA批注进行批注,以将其定义为实体。 WidgetRestController是一个Spring MVC控制器,它将RESTful API调用转换为要在Widgets上执行的动作。 WidgetService是标准的Spring服务,用于定义Widgets业务功能。 最后, WidgetRepository是一个Spring Data JPA接口,Spring将在运行时为此接口创建一个实现。 在下一节中,我们将在编写测试时回顾每个类的代码。

对Spring服务进行单元测试

让我们首先回顾一下如何测试Spring 服务 ,因为这是我们MVC应用程序中最容易测试的组件。 本节中的示例将使我们能够探索JUnit 5与Spring的集成,而无需引入任何新的测试组件或库,尽管我们将在本教程的后面部分进行介绍。

我们将从回顾WidgetService接口和WidgetServiceImpl类开始,分别在清单1和清单2中显示。

清单1. Spring服务接口(WidgetService.java)

package com.geekcap.javaworld.spring5mvcexample.service;

import com.geekcap.javaworld.spring5mvcexample.model.Widget;

import java.util.List;
import java.util.Optional;

public interface WidgetService {
    Optional<Widget> findById(Long id);
    List<Widget> findAll();
    Widget save(Widget widget);
    void deleteById(Long id);
}
清单2. Spring服务实现类(WidgetServiceImpl.java)

package com.geekcap.javaworld.spring5mvcexample.service;

import com.geekcap.javaworld.spring5mvcexample.model.Widget;
import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository;
import com.google.common.collect.Lists;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
public class WidgetServiceImpl implements WidgetService {

    private WidgetRepository repository;

    public WidgetServiceImpl(WidgetRepository repository) {
        this.repository = repository;
    }

    @Override
    public Optional<Widget> findById(Long id) {
        return repository.findById(id);
    }

    @Override
    public List<Widget> findAll() {
        return Lists.newArrayList(repository.findAll());
    }

    @Override
    public Widget save(Widget widget) {
        // Increment the version number
        widget.setVersion(widget.getVersion()+1);

        // Save the widget to the repository
        return repository.save(widget);
    }

    @Override
    public void deleteById(Long id) {
        repository.deleteById(id);
    }
}

WidgetServiceImpl是一个带有@Service注释的Spring服务,该服务具有通过其构造函数连接到其中的WidgetRepositoryfindById()findAll()deleteById()方法都是基础WidgetRepository直通方法。 您将找到的唯一业务逻辑位于save()方法中,该方法将在save() Widget时增加其版本号。

测试班

为了测试该类,我们需要创建并配置一个模拟WidgetRepository ,将其连接到WidgetServiceImpl实例,然后将WidgetServiceImpl到我们的测试类。 幸运的是,这比听起来容易得多。 清单3显示了WidgetServiceTest类的源代码。

清单3. Spring服务测试类(WidgetServiceTest.java)

package com.geekcap.javaworld.spring5mvcexample.service;

import com.geekcap.javaworld.spring5mvcexample.model.Widget;
import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.mockito.Mockito.doReturn;
import static org.mockito.ArgumentMatchers.any;

@SpringBootTest
public class WidgetServiceTest {
    /**
     * Autowire in the service we want to test
     */
    @Autowired
    private WidgetService service;

    /**
     * Create a mock implementation of the WidgetRepository
     */
    @MockBean
    private WidgetRepository repository;

    @Test
    @DisplayName("Test findById Success")
    void testFindById() {
        // Setup our mock repository
        Widget widget = new Widget(1l, "Widget Name", "Description", 1);
        doReturn(Optional.of(widget)).when(repository).findById(1l);

        // Execute the service call
        Optional<Widget> returnedWidget = service.findById(1l);

        // Assert the response
        Assertions.assertTrue(returnedWidget.isPresent(), "Widget was not found");
        Assertions.assertSame(returnedWidget.get(), widget, "The widget returned was not the same as the mock");
    }

    @Test
    @DisplayName("Test findById Not Found")
    void testFindByIdNotFound() {
        // Setup our mock repository
        doReturn(Optional.empty()).when(repository).findById(1l);

        // Execute the service call
        Optional<Widget> returnedWidget = service.findById(1l);

        // Assert the response
        Assertions.assertFalse(returnedWidget.isPresent(), "Widget should not be found");
    }

    @Test
    @DisplayName("Test findAll")
    void testFindAll() {
        // Setup our mock repository
        Widget widget1 = new Widget(1l, "Widget Name", "Description", 1);
        Widget widget2 = new Widget(2l, "Widget 2 Name", "Description 2", 4);
        doReturn(Arrays.asList(widget1, widget2)).when(repository).findAll();

        // Execute the service call
        List<Widget> widgets = service.findAll();

        // Assert the response
        Assertions.assertEquals(2, widgets.size(), "findAll should return 2 widgets");
    }

    @Test
    @DisplayName("Test save widget")
    void testSave() {
        // Setup our mock repository
        Widget widget = new Widget(1l, "Widget Name", "Description", 1);
        doReturn(widget).when(repository).save(any());

        // Execute the service call
        Widget returnedWidget = service.save(widget);

        // Assert the response
        Assertions.assertNotNull(returnedWidget, "The saved widget should not be null");
        Assertions.assertEquals(2, returnedWidget.getVersion(), "The version should be incremented");
    }
}

WidgetServiceTest类使用@SpringBootTest注释进行注释,该注释将扫描CLASSPATH中的所有Spring配置类和bean,并为测试类设置Spring应用程序上下文。 请注意, WidgetServiceTest还通过@SpringBootTest注释隐式包括@ExtendWith(SpringExtension.class)注释,该注释将测试类与JUnit 5集成在一起。

测试类还使用Spring的@Autowired注释自动对要测试的WidgetService进行WidgetService ,并使用Mockito的@MockBean注释创建模拟WidgetRepository 。 此时,我们有一个可以配置的模拟WidgetRepository ,以及一个WidgetService了模拟WidgetRepository的真实WidgetService

测试Spring服务

第一个测试方法testFindById()执行WidgetServicefindById()方法,该方法应返回包含WidgetOptional<Widget> 。 我们首先创建一个我们希望WidgetRepository返回的Widget 。 然后,我们利用Mockito API配置WidgetRepository::findById方法。 模拟逻辑的结构如下:


doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD

在这种情况下,我们说:返回一个Optional我们Widget当资源库的findById()方法被调用为1的参数(作为long )。

接下来,我们使用参数1调用WidgetServicefindById方法。然后验证它是否存在,并且返回的Widget是我们配置的模拟WidgetRepository返回的Widget

翻译自: https://www.infoworld.com/article/3543268/junit-5-tutorial-part-2-unit-testing-spring-mvc-with-junit-5.html

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐