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


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

    本文中的测试均基于JUnit5。

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

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

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

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

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

    单元测试实战(六)其它

    我们常用的DAO层开发框架包括JPA(Spring Boot Data Jpa)和MyBatis-Plus。

    框架都提供根据一定规约自动生成查询/更新的操作,如代理接口。这一部分责任在框架本身,我们不需测试。因此DAO的测试应主要针对自定义查询/更新操作。

    JPA的测试注解是@DataJpaTest,Mybatis-Plus的是@MyBatisPlusTest;它们的思想是一致的,此次先讲JAP。

    概述

    JPA组件表现为Repository对象。如果是Spring Data JPA标准的Repository,且使用接口代理,那么理论上是不需要测试的;但实际中不排除想验证订制代码(native SQL、JPQL、标准查询API,以及胶水代码)的需求。

    JPA测试有专门的@DataJpaTest注解,是Spring boot测试框架提供的功能,因此它是有Spring上下文的,使用JUnit的SpringExtension扩展类。

    测试还是遵循经典三段式:given、when、then;即:假设xxx……那么当yyy时……应该会zzz。

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

    断言应主要检查数据存取行为是否符合预期。

    依赖

    JPA测试除了依赖JUnit和Spring boot测试框架外还依赖一个内存数据库,我们用H2,如下:

    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>
    11. <dependency>
    12. <groupId>com.h2databasegroupId>
    13. <artifactId>h2artifactId>
    14. <scope>testscope>
    15. dependency>

    示例

    以下是用代理接口实现的UserRepository:

    1. package com.aaa.api.auth.repository;
    2. import org.springframework.data.jpa.repository.JpaRepository;
    3. import com.aaa.api.auth.entity.User;
    4. import org.springframework.stereotype.Repository;
    5. import java.util.List;
    6. import java.util.Optional;
    7. @Repository
    8. public interface UserRepository extends JpaRepository {
    9. Optional findByUserCode(String userCode);
    10. List findTop10ByNameContaining(String keyword);
    11. }

    上面的Repository虽然没有订制代码,但不妨碍我们用它来演示Repository测试的写法。

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

    1. package com.aaa.api.auth.repository;
    2. import com.aaa.api.auth.entity.User;
    3. import jakarta.persistence.Query;
    4. import org.junit.jupiter.api.BeforeEach;
    5. import org.junit.jupiter.api.Test;
    6. import org.springframework.beans.factory.annotation.Autowired;
    7. import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
    8. import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
    9. import org.springframework.test.context.TestPropertySource;
    10. import java.util.List;
    11. import static org.assertj.core.api.Assertions.assertThat;
    12. @DataJpaTest
    13. //@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    14. @TestPropertySource(properties = {
    15. //"spring.datasource.driver-class-name=org.h2.Driver",
    16. //"spring.datasource.url=jdbc:h2:mem:api_auth_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL;DATABASE_TO_LOWER=TRUE;NON_KEYWORDS=user",
    17. //"spring.datasource.username=sa",
    18. "spring.jpa.hibernate.ddl-auto=create-drop",
    19. "spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop",
    20. "spring.jpa.properties.hibernate.globally_quoted_identifiers=true"
    21. })
    22. class UserRepositoryTest {
    23. @Autowired
    24. private UserRepository repo;
    25. @Autowired
    26. private TestEntityManager entityManager;
    27. private final User u1 = new User();
    28. private final User u2 = new User();
    29. private final User u3 = new User();
    30. @BeforeEach
    31. void setUp() {
    32. u1.setName("张三");
    33. u1.setUserCode("zhangsan");
    34. u1.setRole(User.ADMIN);
    35. u1.setEmail("zhangsan@aaa.net.cn");
    36. u1.setMobile("13600001234");
    37. u2.setName("李四");
    38. u2.setUserCode("lisi");
    39. u2.setRole(User.ADMIN);
    40. u2.setEmail("lisi@aaa.net.cn");
    41. u2.setMobile("13800001234");
    42. u3.setName("王五");
    43. u3.setUserCode("wangwu");
    44. u3.setRole(User.USER);
    45. u3.setEmail("wangwu@aaa.net.cn");
    46. u3.setMobile("13900001234");
    47. }
    48. @Test
    49. void testSave() {
    50. Query q = entityManager.getEntityManager().createQuery("from User");
    51. repo.save(u1);
    52. assertThat(q.getResultList()).hasSize(1);
    53. repo.save(u2);
    54. assertThat(q.getResultList()).hasSize(2);
    55. User u = repo.save(u3);
    56. assertThat(q.getResultList()).hasSize(3);
    57. u3.setRole(User.ADMIN);
    58. repo.save(u3);
    59. assertThat(q.getResultList()).hasSize(3);
    60. assertThat(entityManager.find(User.class, u.getId()).getRole()).isEqualTo(User.ADMIN);
    61. }
    62. @Test
    63. void testFindAll() {
    64. entityManager.persist(u1);
    65. entityManager.persist(u2);
    66. entityManager.persist(u3);
    67. List entities = repo.findAll();
    68. assertThat(entities).size().isEqualTo(3);
    69. assertThat(entities.get(0).getId()).isNotNull();
    70. assertThat(entities.get(1).getId()).isNotNull();
    71. assertThat(entities.get(2).getId()).isNotNull();
    72. }
    73. @Test
    74. void testFindByUserCode() {
    75. entityManager.persist(u1);
    76. entityManager.persist(u2);
    77. entityManager.persist(u3);
    78. User u1 = repo.findByUserCode("zhangsan").orElse(null);
    79. assertThat(u1).isNotNull();
    80. assertThat(u1.getRole()).isEqualTo(User.ADMIN);
    81. User u2 = repo.findByUserCode("lisi").orElse(null);
    82. assertThat(u2).isNotNull();
    83. assertThat(u2.getRole()).isEqualTo(User.ADMIN);
    84. User u3 = repo.findByUserCode("wangwu").orElse(null);
    85. assertThat(u3).isNotNull();
    86. assertThat(u3.getRole()).isEqualTo(User.USER);
    87. u3.setRole(User.ADMIN);
    88. entityManager.persist(u3);
    89. u3 = repo.findByUserCode("wangwu").orElse(null);
    90. assertThat(u3).isNotNull();
    91. assertThat(u3.getRole()).isEqualTo(User.ADMIN);
    92. }
    93. @Test
    94. void testFindTop10ByNameContaining() {
    95. entityManager.persist(u2);
    96. entityManager.persist(u3);
    97. List users = repo.findTop10ByNameContaining("张三");
    98. assertThat(users).isEmpty();
    99. users = repo.findTop10ByNameContaining("李四");
    100. assertThat(users).hasSize(1);
    101. users = repo.findTop10ByNameContaining("五");
    102. assertThat(users).hasSize(1);
    103. }
    104. }

    测试类说明:

    第16行,标注本测试为@DataJpaTest。

    18-25行是数据源和JPA属性的订制。

    @DataJpaTest、@WebMvcTest以及@SpringBootTest等测试其实是需要Spring Boot的Configuration的;因此我们在测试目录里可能(尤其在多模块工程里)有个专用于测试的Mock Application,注解为@SpringBootApplication(空类,不需要main方法),并且在src/test/resources下还可以有application.properties(或yaml)。而更灵活的方式是使用第18行的@TestPropertySource的properties属性。

    @DataJpaTest这个注解会在测试时自动将数据源替换为内存数据库H2的;如果不想要这种替换,或者要订制其url等属性,那么就可以将第17行、第19-21行的注释放开。

    第24行的spring.jpa.properties.hibernate.globally_quoted_identifiers=true可以避免数据库保留字与表名/列名冲突。

    第29行,我们将待测试类对象作为测试类的一个属性,并使用@Autowired进行注入。

    第32行,我们注入了一个TestEntityManager;它作为一个实用工具,帮我们在测试中插数据、查数据,这样就避免了直接使用repo的方法插数据、查数据,因为这些方法本身就是待测目标。

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

    从59行开始,是测试方法。该测试类中,没有任何Mock对象,因此也就不存在given - when - then三段式结构。测试方法都是直接对测试数据进行CRUD操作并检查操作结果,代码都是自解释的。

    总结

    对于JPA Repository的测试,推荐使用@DataJpaTest注解。

    如需订制内存数据库(datasource)的属性,则令@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE),然后在@TestPropertySource.properties里写订制属性。

    Repository应设计为单纯的对数据实体的CRUD操作,因此通常不需Mock任何依赖对象。

  • 相关阅读:
    PeopleCode中Date函数的用法
    Excel第28享:如何新建一个Excel表格
    PHP如何对二维数组(多维数组)进排序
    互联网医疗产品如何最大程度上避免和制约号贩子占号行为?
    用MiniPC搭建个人服务器
    yolov7简化网络yaml配置文件
    MySQL使用全文索引+ngram全文解析器进行全文检索
    【LeetCode】215. 数组中的第K个最大元素
    python通过生成器实现协程-生产消费者模型
    Redis做接口限流
  • 原文地址:https://blog.csdn.net/ioriogami/article/details/134481252