• 单元测试实战(二)Service 的测试


    为鼓励单元测试,特分门别类示例各种组件的测试代码并进行解说,供开发人员参考。

    本文中的测试均基于JUnit5。

    单元测试实战(一)Controller 的测试

    单元测试实战(二)Service 的测试

    单元测试实战(三)JPA 的测试    

    单元测试实战(四)MyBatis-Plus 的测试

    单元测试实战(五)普通类的测试

    单元测试实战(六)其它

    概述

    与Controller不同,Service的测试可以脱离Spring上下文环境。这是因为Controller测试需要覆盖从HTTP请求到handler方法的路由,即需要SpringMvc的介入;而Service则是一种比较单纯的类,可以当做简单对象来测试。

    我们将使用JUnit的MockitoExtension扩展来对Service对象进行测试。待测试对象为测试类的一个属性。测试仍遵循经典三段式:given、when、then;即:假设xxx……那么当yyy时……应该会zzz。

    在每个测试之前应清理/重置测试数据,即操作的业务实体。

    断言应主要检查Service的行为是否符合预期。

    依赖

    需要的依赖与Controller测试需要的依赖相同:

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-testartifactId>
    4. <scope>testscope>
    5. dependency>
    6. <dependency>
    7. <groupId>org.junit.jupitergroupId>
    8. <artifactId>junit-jupiter-apiartifactId>
    9. <scope>testscope>
    10. dependency>

    示例

    以下是UserService的实现类UserServiceImpl。接口定义省略(从@Override注解不难推出)。

    1. package com.aaa.api.auth.service.impl;
    2. import com.aaa.api.auth.entity.User;
    3. import com.aaa.api.auth.repository.UserRepository;
    4. import com.aaa.api.auth.service.UserService;
    5. import org.springframework.stereotype.Service;
    6. import java.time.Instant;
    7. import java.util.List;
    8. @Service
    9. public class UserServiceImpl implements UserService {
    10. private final UserRepository repo;
    11. public UserServiceImpl(UserRepository repo) {
    12. this.repo = repo;
    13. }
    14. @Override
    15. public User findById(Long id) {
    16. return repo.findById(id).orElse(null);
    17. }
    18. @Override
    19. public User findByUserCode(String userCode) {
    20. return repo.findByUserCode(userCode).orElse(null);
    21. }
    22. @Override
    23. public User save(User user) {
    24. user.setGmtModified(Instant.now());
    25. return repo.save(user);
    26. }
    27. @Override
    28. public List findAll() {
    29. return repo.findAll();
    30. }
    31. }

    以下是对UserServiceImpl进行测试的测试类:

    1. package com.aaa.api.auth.service;
    2. import com.aaa.api.auth.entity.User;
    3. import com.aaa.api.auth.repository.UserRepository;
    4. import com.aaa.api.auth.service.impl.UserServiceImpl;
    5. import org.junit.jupiter.api.BeforeEach;
    6. import org.junit.jupiter.api.Test;
    7. import org.junit.jupiter.api.extension.ExtendWith;
    8. import org.mockito.InjectMocks;
    9. import org.mockito.Mock;
    10. import org.mockito.junit.jupiter.MockitoExtension;
    11. import java.util.List;
    12. import java.util.Optional;
    13. import static org.assertj.core.api.Assertions.assertThat;
    14. import static org.junit.jupiter.api.Assertions.assertThrows;
    15. import static org.mockito.ArgumentMatchers.any;
    16. import static org.mockito.BDDMockito.given;
    17. @ExtendWith(MockitoExtension.class)
    18. class UserServiceTest {
    19. @Mock
    20. private UserRepository repo;
    21. @InjectMocks
    22. private UserServiceImpl svc;
    23. private final User u1 = new User();
    24. private final User u2 = new User();
    25. private final User u3 = new User();
    26. @BeforeEach
    27. void setUp() {
    28. u1.setName("张三");
    29. u1.setUserCode("zhangsan");
    30. u1.setRole(User.ADMIN);
    31. u1.setEmail("zhangsan@aaa.net.cn");
    32. u1.setMobile("13600001234");
    33. u2.setName("李四");
    34. u2.setUserCode("lisi");
    35. u2.setRole(User.ADMIN);
    36. u2.setEmail("lisi@aaa.net.cn");
    37. u2.setMobile("13800001234");
    38. u3.setName("王五");
    39. u3.setUserCode("wangwu");
    40. u3.setRole(User.USER);
    41. u3.setEmail("wangwu@aaa.net.cn");
    42. u3.setMobile("13900001234");
    43. }
    44. @Test
    45. void testFindById() {
    46. // given - precondition or setup
    47. given(repo.findById(1L)).willReturn(Optional.of(u1));
    48. // when - action or the behaviour that we are going test
    49. User found = svc.findById(1L);
    50. // then - verify the output
    51. assertThat(found).isNotNull();
    52. assertThat(found.getUserCode()).isEqualTo("zhangsan");
    53. }
    54. @Test
    55. void testFindByIdNegative() {
    56. // given - precondition or setup
    57. given(repo.findById(1L)).willReturn(Optional.empty());
    58. // when - action or the behaviour that we are going test
    59. User found = svc.findById(1L);
    60. // then - verify the output
    61. assertThat(found).isNull();
    62. }
    63. @Test
    64. void testFindByUserCode() {
    65. // given - precondition or setup
    66. given(repo.findByUserCode(any())).willReturn(Optional.of(u1));
    67. // when - action or the behaviour that we are going test
    68. User found = svc.findByUserCode("zhangsan");
    69. // then - verify the output
    70. assertThat(found).isNotNull();
    71. assertThat(found.getUserCode()).isEqualTo("zhangsan");
    72. }
    73. @Test
    74. void testFindByUserCodeNegative() {
    75. // given - precondition or setup
    76. given(repo.findByUserCode(any())).willReturn(Optional.empty());
    77. // when - action or the behaviour that we are going test
    78. User found = svc.findByUserCode("zhangsan");
    79. // then - verify the output
    80. assertThat(found).isNull();
    81. }
    82. @Test
    83. void testSave() {
    84. // given - precondition or setup
    85. given(repo.save(any(User.class))).willAnswer((invocation -> invocation.getArguments()[0]));
    86. // when - action or the behaviour that we are going test
    87. User saved = svc.save(u1);
    88. // then - verify the output
    89. assertThat(saved).isNotNull();
    90. assertThat(saved.getGmtModified()).isNotNull();
    91. }
    92. @Test
    93. void testSaveNegative() {
    94. // given - precondition or setup
    95. given(repo.save(any())).willThrow(new RuntimeException("Testing"));
    96. // when - action or the behaviour that we are going test
    97. // User saved = svc.save(u1);
    98. // then - verify the output
    99. assertThrows(RuntimeException.class, () -> svc.save(u1));
    100. }
    101. @Test
    102. void testFindAll() {
    103. // given - precondition or setup
    104. given(repo.findAll()).willReturn(List.of(u1, u2, u3));
    105. // when - action or the behaviour that we are going test
    106. List found = svc.findAll();
    107. // then - verify the output
    108. assertThat(found).isNotNull();
    109. assertThat(found.size()).isEqualTo(3);
    110. }
    111. }

    测试类说明:

    第22行,我们使用了JUnit的MockitoExtension扩展。

    第26行,我们Mock了一个UserRepository类型的对象repo,它是待测UserServiceImpl对象的依赖。由于脱离了Spring环境,所以它是个@Mock,不是@MockBean。

    接着,第29行,就是待测对象svc。它有个注解@InjectMocks,意思是为该对象进行依赖注入(Mockito提供的功能);于是,repo就被注入到svc里了。

    第31-33行提供了三个测试数据,并在setUp()方法中进行初始化/重置。@BeforeEach注解使得setUp()方法在每个测试之前都会执行一遍。

    接下来,从56行开始,是测试方法;每个方法都遵循given - when - then三段式。

    testFindById方法是测试根据id获取User对象的。它假设repository的findById(1)会返回对象u1;那么当调用svc.findById(1)时;返回的实体就应该是u1。

    testFindByIdNegative方法是根据id获取User对象的负面测试。它假设找不到ID为1的User,即repository的findById(1)会返回空;那么当调用svc.findById(1)时;返回的实体应该为空。

    testFindByUserCode、testFindByUserCodeNegative与testFindById、testFindByIdNegative一样,只不过查询条件换成userCode,不再赘述。

    testSave方法是测试保存User对象的。它假设repository的save()方法在保存任何User对象时都会返回该对象本身;那么当调用svc.save(u1)时;返回的实体应该为u1。注意在这里我们assert了gmtModified属性,以确认UserServiceImpl.save()方法里对该属性的设置。

    testSaveNegative方法是保存User对象的负面测试。它假设repository的save()方法会抛出运行时异常;那么当调用svc.save(u1)时;会接到这个异常。

    testFindAll方法是测试获取所有User对象的,它假设repository的findAll()会返回对象u1、u2、u3;那么当调用svc.findAll()时;就应返回全部三个对象。

    总结

    Service的测试,推荐使用@ExtendWith(MockitoExtension.class),脱离Spring上下文,使用纯Mockito打桩。其它方面,理念均与Controller测试一样。

    虽然Service测试的打桩器较简单,但由于业务逻辑可能都位于这一层,需要覆盖的场景多,测试用例也应该多。Service层的测试是所有层中最重要的。

  • 相关阅读:
    计算机网络 静态路由及动态路由RIP
    Upstream Consistent Hash
    [2022 广东省赛M] 拉格朗日插值 (多元函数极值 分治NTT)
    北方经贸杂志北方经贸杂志社北方经贸编辑部2022年第10期目录
    Autosar Configuration(九) Security之不修改DBC属性配置SecOC安全报文、同步报文和同步请求报文
    无敌,全面对标字节跳动2-2:算法与数据结构突击手册(leetcode)
    Spring5(两万五字)
    【JavaEE进阶序列 | 从小白到工程师】String类常用的成员方法,一文直接上手使用
    【错误记录】安装 Hadoop 运行环境报错 ( Error: JAVA_HOME is incorrectly set. Please update xxx\hadoop-env.cmd )
    【大语言模型LLM】-如何使用大语言模型提高工作效率?
  • 原文地址:https://blog.csdn.net/ioriogami/article/details/134481160