likes
comments
collection
share

重构数据访问之引入 JPA 审计与独立测试策略

作者站长头像
站长
· 阅读数 26

JPA 审计

数据访问优化

回到这个遗留系统,之前采用了 JPA 作为数据访问标准,估计也是看到了语义化函数命名、接口标准统一、与 Spring Boot 亲和性好等优点。

还有几个值得优化的地方,方便工程扩展。比如测试便利性,以前只有集成测试,也就是要连接到测试环境数据库才能做测试,并且启动了整个 Spring 容器,不仅慢而且强依赖,无法验证一些方法内部逻辑等小改动。再比如对象复用没有充分利用 JPA 的基础能力,所有数据实体都用到了修改用户、修改时间这样的字段造成大量重复,没有利用 JPA 审计(Auditing)这样的功能进行抽象复用,返回结果聚合也可以利用 JPA Map 进行简单映射。

下面我们就来逐个解决,基于 Spring Boot 2.5 完成,经过测试 2.2 以上版本都适用。

引入 JPA 审计

Spring Data 审计功能提供了透明跟踪创建与修改实体的用户和时间,Spring Boot 启用审计可以直接在主类上添加这个注解。

@EnableJpaAuditing

审计需要配置元数据,也就是告知 JPA 哪些属性需要跟踪与修改。

@EntityListeners({AuditingEntityListener.class})

一种配置基于注解,@CreatedBy、@LastModifiedBy、@CreatedDate 和 @LastModifiedDate 标记创建用户、修改用户、创建时间和修改时间。

另一种是基于接口,实现 Auditable 接口,设置这四种审计属性。而 Spring Data 的设计者也是考虑到大部分情况下是不需要动态获取时间如何变更,Web 等应用却需要动态感知当前访问用户是谁,因此又提供了 AuditorAware 接口,只对用户获取做扩展。

注解配置审计属性

接下来,添加 BaseEntity,所有需要审计属性的 Entity 都可以继承它。

@Getter
@MappedSuperclass
@EntityListeners({AuditingEntityListener.class})
public abstract class BaseEntity {

    @Id
    @GenericGenerator(name = "fendo_Generator", strategy = "identity")
    @GeneratedValue(generator = "fendo_Generator")
    private Long id;

    @CreatedBy
    @Column(name = "create_user")
    private String createUser;

    @LastModifiedBy
    @Column(name = "update_user")
    private String updateUser;

    @CreatedDate
    @Column(name = "create_time")
    private LocalDateTime createTime;

    @LastModifiedDate
    @Column(name = "update_time")
    private LocalDateTime updateTime;
}

然后,实现 AuditorAware 接口获取当前用户,比如 Web 应用可以使用拦截器从 Session 中获取当前用户。

@Configuration
public class ModifierUserAware implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of(UserAccountFilter.getUserCode());
    }
}

JPA 测试

单元测试

Spring Boot 为测试应用程序片段提供了很多名称如 @...Test 这样的注解,他们会为测试片段准备好自动配置注解,测试 JPA 则提供了 @DataJpaTest,使用起来很方便。

首先,确认添加测试数据库依赖,Spring Data JPA 默认使用 h2。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

然后,在测试类上添加 @DataJpaTest 注解,这个注解用于加载 Spring Data JPA 所需上下文,包括扫描 Repository,以及默认集成 h2 作为测试数据库。

最后,就可以对 Repository 进行局部测试了,这样做的好处是可以独立测试 Repository,不需要连接外部数据库,也不需要加载所有 Bean。@DataJpaTest 默认是开启 SQL 日志打印的(spring.jpa.show-sql),可以直接在终端查看 SQL 执行过程:

Hibernate: select hotelfollo0_.id as id1_1_, hotelfollo0_.create_time as create_t2_1_, hotelfollo0_.create_user as create_u3_1_, hotelfollo0_.update_time as update_t4_1_, hotelfollo0_.update_user as update_u5_1_, hotelfollo0_.hotel_id as hotel_id6_1_ from followed_hotel hotelfollo0_
Hibernate: select nextval ('fendo_generator')
Hibernate: insert into followed_hotel (create_time, create_user, update_time, update_user, hotel_id, id) values (?, ?, ?, ?, ?, ?)
Hibernate: select hotelfollo0_.id as id1_1_, hotelfollo0_.create_time as create_t2_1_, hotelfollo0_.create_user as create_u3_1_, hotelfollo0_.update_time as update_t4_1_, hotelfollo0_.update_user as update_u5_1_, hotelfollo0_.hotel_id as hotel_id6_1_ from followed_hotel hotelfollo0_

审计测试

用户配置在架构设计上是与数据访问是分离的,测试上也是如此,不应在数据访问层完成,隔离审计用户获取的方式,可以使用 MockBean

上文提到的 ModifierUserAware 获取当前请求数据访问用户就可以这样实现测试隔离:

@MockBean
private ModifierUserAware modifierUserAware;

@BeforeEach
public void testModifierUser() {
    changeModifierUser(DEFAULT_USER_CODE);
}

private void changeModifierUser(String userCode) {
    Mockito.when(modifierUserAware.getCurrentAuditor()).thenReturn(Optional.of(userCode));
}

testModifierUser 添加了 @BeforeEach,用于执行数据访问测试前构造访问用户信息,测试不同用户访问数据,就可以调用 changeModifyUser 修改。

最后,实现测试数据的结构就变成了这样,构造测试数据、修改访问用户、再次构造测试数据、执行 Repository 数据访问、断言验证结果。

@DisplayName(xxx)
@Test
public void testxxx() {
    mockDataxxx();
    changeModifierUser(xxx);
    mockDataxxx();
    xxxRepository.xxx()
    Assertions.assertxxx();
}

集成测试

这里对集成测试的理解是连接外部服务,使用测试环境(与生产环境相同的数据库)进行测试。集成测试可用于 H2 在语法上以及 JPA 对不同方言的支持上的差异。

在测试类 @DataJpaTest 同时添加这个注解,表示不使用默认测试数据库替换,会加载外部配置中的数据连接配置。

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

JUnit4 到 JUnit 5

从 Spring Boot 2.2 版本开始,Spring Boot Test 默认使用 JUnit 5 作为测试框架,JUnit 5 与 JUnit 4 在使用上略有不同,常见的如:

  • 引用路径变化,比如 org.junit.junit.Test 变成了 org.junit.jupiter.api.Test
  • 测试注解变化,比如 @After 变成了 @AfterEach,@BeforeClass 变成了 @BeforeAll
  • 引用测试上下文变化,由 @RunWith(SpringRunner.class) 变成了 @ExtendWith(SpringExtension.class)
  • 断言使用方法变化,补充了大量断言方法,比如 Assert.assertEquals() 变成了 Assertions.assertEquals()
  • 增加 @DisplayName 注解,与 IDE 集成测试用例体验得到提升
转载自:https://juejin.cn/post/7366100736146767922
评论
请登录