乐趣区

关于springboot:Spring单元测试教程JUnit5Mockito

1. 测试

如果项目组有测试团队,最常接触的概念有:功能测试、回归测试、冒烟测试等,但这些都是由测试人员发动的。

开发人员写的经常是“单元测试”,但其实能够细分成 单元测试 集成测试 两个。

划分的起因拿常见的 Spring IoC 举例。Spring 不同 Bean 之间相互依赖,例如某 API 业务逻辑中会依赖不同模块的 Service,Service 办法中又可能依赖不同的 Dao 层办法,甚至还会通过 RPC、HTTP 调用内部服务办法。这给咱们写测试用例带来了难度,原本只想测试某个办法的性能,却要思考一连串的依赖关系。

1.1. 单元测试

单元测试:是指对软件中的最小可测试单元进行检查和验证。

通常任何软件都会划分为不同的模块和组件。独自测试一个组件时,咱们叫做单元测试。单元测试用于验证相干的一小段代码是否失常工作。单元测试不是用于发现应用程序范畴内的 bug,或者回归测试的 bug,而是别离检测每个代码片段。

单元测试不验证利用程序代码是否和内部依赖失常工作。它聚焦与单个组件并且 Mock 所有和它交互的依赖。例如,办法中调用发短信的服务,以及和数据库的交互,咱们只须要 Mock 假执行即可,毕竟测试的焦点在以后办法上。

单元测试的特点:

  • 不依赖任何模块。
  • 基于代码的测试,不须要在 ApplicationContext 中运行。
  • 办法执行快,500ms 以内(也和不启动 Spring 无关)。
  • 同一单元测试可反复执行 N 次,并每次运行后果雷同。

1.2. 集成测试

集成测试:在单元测试的根底上,将所有模块依照设计要求组装成为子系统或零碎,进行集成测试。

集成测试次要用于发现用户端到端申请时不同模块交互产生的问题。集成测试范畴能够是整个应用程序,也能够是一个独自的模块,取决于要测试什么。

在集成测试中,咱们应该聚焦于从控制器层到长久层的残缺申请。应用程序应该运行嵌入服务(例如:Tomcat)以创立应用程序上下文和所有 bean。这些 bean 有的可能会被 Mock 笼罩。

集成测试的特点:

  • 集成测试的目标是测试不同的模块一共工作是否达到预期。
  • 应用程序应该在 ApplicationContext 中运行。Spring boot 提供 @SpringBootTest 注解创立运行上下文。
  • 应用 @TestConfiguration 等配置测试环境。

2. 测试框架

2.1. spring-boot-starter-test

SpringBoot 中无关测试的框架,次要来源于 spring-boot-starter-test。一旦依赖了 spring-boot-starter-test,上面这些类库将被一起依赖进去:

  • JUnit:java 测试事实上的规范。
  • Spring Test & Spring Boot Test:Spring 的测试反对。
  • AssertJ:提供了流式的断言形式。
  • Hamcrest:提供了丰盛的 matcher。
  • Mockito:mock 框架,能够按类型创立 mock 对象,能够依据办法参数指定特定的响应,也反对对于 mock 调用过程的断言。
  • JSONassert:为 JSON 提供了断言性能。
  • JsonPath:为 JSON 提供了 XPATH 性能。

测试环境自定义 Bean

  • @TestComponent:该注解是另一种 @Component,在语义上用来指定某个 Bean 是专门用于测试的。该注解实用于测试代码和正式混合在一起时,不加载被该注解形容的 Bean,应用不多。
  • @TestConfiguration:该注解是另一种 @TestComponent,它用于补充额定的 Bean 或笼罩已存在的 Bean。在不批改正式代码的前提下,使配置更加灵便。

2.2. JUnit

前者说了,JUnit 是一个 Java 语言的单元测试框架,但同样可用于集成测试。以后最新版本是 JUnit5。

常见区别有:

  • JUnit4 所需 JDK5+ 版本即可,而 JUnit5 需 JDK8+ 版本,因而反对很多 Lambda 办法。
  • JUnit 4 将所有内容捆绑到单个 jar 文件中。Junit 5 由 3 个子我的项目组成,即 JUnit Platform,JUnit Jupiter 和 JUnit Vintage。外围是 JUnit Jupiter,它具备所有新的 junit 正文和 TestEngine 实现,以运行应用这些正文编写的测试。而 JUnit Vintage 蕴含对 JUnit3、JUnit4 的兼容,所以 spring-boot-starter-test 新版本 pom 中往往会主动 exclusion 它。
  • SpringBoot 2.2.0 开始引入 JUnit5 作为单元测试默认库,在 SpringBoot 2.2.0 之前,spring-boot-starter-test 蕴含了 JUnit4 的依赖,SpringBoot 2.2.0 之后替换成了 Junit Jupiter。

JUnit5 和 JUnit4 在注解上的区别在于:

性能 JUnit4 JUnit5
申明一种测试方法 @Test @Test
在以后类中的所有测试方法之前执行 @BeforeClass @BeforeAll
在以后类中的所有测试方法之后执行 @AfterClass @AfterAll
在每个测试方法之前执行 @Before @BeforeEach
在每个测试方法之后执行 @After @AfterEach
禁用测试方法 / 类 @Ignore @Disabled
测试工厂进行动静测试 NA @TestFactory
嵌套测试 NA @Nested
标记和过滤 @Category @Tag
注册自定义扩大 NA @ExtendWith

RunWith 和 ExtendWith

在 JUnit4 版本,在测试类加 @SpringBootTest 注解时,同样要加上 @RunWith(SpringRunner.class)才失效,即:

@SpringBootTest
@RunWith(SpringRunner.class)
class HrServiceTest {...}

但在 JUnit5 中,官网告知 @RunWith 的性能都被 @ExtendWith 代替,即原 @RunWith(SpringRunner.class) 被同性能的 @ExtendWith(SpringExtension.class) 代替。但 JUnit5 中 @SpringBootTest 注解中曾经默认蕴含了 @ExtendWith(SpringExtension.class)。

因而,在 JUnit5 中只须要独自应用 @SpringBootTest 注解即可。其余须要自定义拓展的再用 @ExtendWith,不要再用 @RunWith 了。

2.3. Mockito

测试驱动的开发(TDD)要求咱们先写单元测试,再写实现代码。在写单元测试的过程中,咱们往往会遇到要测试的类有很多依赖,这些依赖的类 / 对象 / 资源又有别的依赖,从而造成一个大的依赖树。而 Mock 技术的目标和作用是模仿一些在利用中不容易结构或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。

Mock 框架有很多,除了传统的 EasyMock、Mockito 以外,还有 PowerMock、JMock、JMockit 等。这里选用 Mockito,是因为 Mockito 在社区风行度较高,而且是 SpringBoot 默认集成的框架。

Mockito 框架中最外围的两个概念就是 MockStub。测试时不是真正的操作内部资源,而是通过自定义的代码进行模仿操作。咱们能够对任何的依赖进行模仿,从而使测试的行为不须要任何筹备工作或者不具备任何副作用。

当咱们在测试时,如果只关怀某个操作是否执行过,而不关怀这个操作的具体行为,这种技术称为 mock。比方咱们测试的代码会执行发送邮件的操作,咱们对这个操作进行 mock;测试的时候咱们只关怀是否调用了发送邮件的操作,而不关怀邮件是否的确发送进来了。

另一种状况,当咱们关怀操作的具体行为,或者操作的返回后果的时候,咱们通过执行预设的操作来代替指标操作,或者返回预设的后果作为指标操作的返回后果。这种对操作的模仿行为称为 stub(打桩)。比方咱们测试代码的异样解决机制是否失常,咱们能够对某处代码进行 stub,让它抛出异样。再比方咱们测试的代码须要向数据库插入一条数据,咱们能够对插入数据的代码进行 stub,让它始终返回 1,示意数据插入胜利。

举荐一个 Mockito 中文文档。

mock 和 spy 的区别

mock 办法和 spy 办法都能够对对象进行 mock。然而前者是接管了对象的全副办法,而后者只是将有桩实现(stubbing)的调用进行 mock,其余办法依然是理论调用。

如下例,因为只 mock 了 List.size()办法。如果 mockList 是通过 mock 生成的,List 的 add、get 等其余办法都生效,返回的数据都为 null。但如果是通过 spy 生成的,则验证失常。

平时开发过程中,咱们通常只须要 mock 类的某些办法,用 spy 即可。

@Test
    void mockAndSpy() {List<String> mockList = Mockito.mock(List.class);
        // List<String> mockList = Mockito.spy(new ArrayList<>());
        Mockito.when(mockList.size())
                .thenReturn(100);

        mockList.add("A");
        mockList.add("B");
        Assertions.assertEquals("A", mockList.get(0));
        Assertions.assertEquals(100, mockList.size());
    }

3. 示例

3.1. 单元测试示例

因为 JUnit5、Mockito 都是 spring-boot-starter-test 默认依赖的,所以 pom 中无需引入其余非凡依赖。先写个简略的 Service 层办法,通过两张表查问数据。
HrService.java

@AllArgsConstructor
@Service
public class HrService {
    private final OrmDepartmentDao ormDepartmentDao;
    private final OrmUserDao ormUserDao;

    List<OrmUserPO> findUserByDeptName(String deptName) {return ormDepartmentDao.findOneByDepartmentName(deptName)
                .map(OrmDepartmentPO::getId)
                .map(ormUserDao::findByDepartmentId)
                .orElse(Collections.emptyList());
    }
}

IDEA 创立测试类

接下来针对该 Service 类创立测试类,咱们应用的开发工具是 IDEA。点进以后类,右键 ->Go To->Test->Create New Test,在 Testing library 中抉择 Junit5,则在对应目录生成测试类和办法。

HrServiceTest.java

@ExtendWith(MockitoExtension.class)
class HrServiceTest {
    @Mock
    private OrmDepartmentDao ormDepartmentDao;
    @Mock
    private OrmUserDao ormUserDao;
    @InjectMocks
    private HrService hrService;

    @DisplayName("依据部门名称,查问用户")
    @Test
    void findUserByDeptName() {
        Long deptId = 100L;
        String deptName = "行政部";
        OrmDepartmentPO ormDepartmentPO = new OrmDepartmentPO();
        ormDepartmentPO.setId(deptId);
        ormDepartmentPO.setDepartmentName(deptName);
        OrmUserPO user1 = new OrmUserPO();
        user1.setId(1L);
        user1.setUsername("001");
        user1.setDepartmentId(deptId);
        OrmUserPO user2 = new OrmUserPO();
        user2.setId(2L);
        user2.setUsername("002");
        user2.setDepartmentId(deptId);
        List<OrmUserPO> userList = new ArrayList<>();
        userList.add(user1);
        userList.add(user2);

        Mockito.when(ormDepartmentDao.findOneByDepartmentName(deptName))
                .thenReturn(Optional.ofNullable(ormDepartmentPO)
                                .filter(dept -> deptName.equals(dept.getDepartmentName()))
                );
        Mockito.doReturn(userList.stream()
                        .filter(user -> deptId.equals(user.getDepartmentId()))
                        .collect(Collectors.toList())
        ).when(ormUserDao).findByDepartmentId(deptId);

        List<OrmUserPO> result1 = hrService.findUserByDeptName(deptName);
        List<OrmUserPO> result2 = hrService.findUserByDeptName(deptName + "error");

        Assertions.assertEquals(userList, result1);
        Assertions.assertEquals(Collections.emptyList(), result2);
    }

因为单元测试不必启动 Spring 容器,则无需加 @SpringBootTest,因为要用到 Mockito,只须要自定义拓展 MockitoExtension.class 即可,依赖简略,运行速度更快。

能够显著看到,单元测试写的代码,怎么是被测试代码长度的好几倍?其实单元测试的代码长度比拟固定,都是造数据和打桩,但如果针对越简单逻辑的代码写单元测试,还是越划算的。

3.2. 集成测试示例

还是那个办法,如果应用 Spring 上下文,实在的调用办法依赖,可间接用下列形式:

@SpringBootTest
class HrServiceTest {
    @Autowired
    private HrService hrService;

    @DisplayName("依据部门名称,查问用户")
    @Test
    void findUserByDeptName() {List<OrmUserPO> userList = hrService.findUserByDeptName("行政部");
        Assertions.assertTrue(userList.size() > 0);
    }  
}

还能够应用 @MockBean@SpyBean 替换 Spring 上下文中的对应的 Bean:

@SpringBootTest
class HrServiceTest {
    @Autowired
    private HrService hrService;
    @SpyBean
    private OrmDepartmentDao ormDepartmentDao;

    @DisplayName("依据部门名称,查问用户")
    @Test
    void findUserByDeptName() {
        String deptName="行政部";
        OrmDepartmentPO ormDepartmentPO = new OrmDepartmentPO();
        ormDepartmentPO.setDepartmentName(deptName);
        Mockito.when(ormDepartmentDao.findOneByDepartmentName(ArgumentMatchers.anyString()))
                .thenReturn(Optional.of(ormDepartmentPO));
        List<OrmUserPO> userList = hrService.findUserByDeptName(deptName);
        Assertions.assertTrue(userList.size() > 0);
    }
}

小提示:@SpyBean 和 spring boot data 的问题

当用 @SpyBean 增加到 spring data jpa 的 dao 层上时(继承 JpaRepository 的接口),会无奈启动容器,报错 org.springframework.beans.factory.BeanCreationException: Error creating bean with name。包含 mongo 等 spring data 都会有此问题,是 spring boot 官网不反对,可查看 Issues-7033,已在 spring boot 2.5.3 版本修复。

退出移动版