分类
Spring Persistence Testing

Spring JPA使用内存数据库进行测试

Self-Contained Testing Using an In-Memory Database

1. 概述

本文将构建一个简单的Spirng应用,并在该应用中使用内存数据库进行单元测试。

往往在开发过程中我们习惯于使用与生产环境相同的数据库,比如MySQL。这就要求在进行开发时,必须在本地准备一个与生产环境相同的数据库环境。

在进行单元测试时,由于每个测试用例都是测试的某一小部分功能,这决定了大多数的单元测试可以不依赖于与生产环境相同的数据库。此时,为这部分单元测试提供快速、轻量化的内存数据库便有了必要。

下面,我们将共同学习如何在单元测试中使用h2内存型数据库来替代MySQL数据库。

2. Maven依赖

我们在Spring Boot(2.3.3.release)项目中的pom.xml中加入以下依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
	<exclusions>
		<exclusion>
			<groupId>org.junit.vintage</groupId>
			<artifactId>junit-vintage-engine</artifactId>
		</exclusion>
	</exclusions>
</dependency>

3. 数据模型及仓库

我们首先创建一个Student学生实体

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    // 空构造函数及setter/getter请自行补充
}

然后创建该Student学生实体对应的数据仓库StudentRepository

public interface StudentRepository extends JpaRepository<Student, Long> {
}

4. 为测试指定H2数据库

为测试指定与运行不同的数据库,我们需要为测试指定一个特定的配置文件。然后设置该配置文件仅在测试环境下生效。

Spring Boot的配置文件位于src/main/resources文件夹中。如果要定义专门用于单元测试的配置文件,则应该将其放置到src/test/resources文件夹中。

Spring在运行单元测试时,首先在src/test/resources查找相应的配置文件。如果在文件夹中没有找到,则会使用src/main/resources文件夹中的文件;如果找到了,则会使文件下的文件而跳过src/main/resources文件夹的查找。

4.1 测试专用配置

如果我们所有的单元测试都想依赖于H2数据库(这虽不是一个好的选择,但有时候在快速切换数据环境时很有效),则可以直接在src/test/resources新建application.properties,并在该文件中将数据数连接信息设置为H2:

spring.datasource.url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
spring.datasource.username=root
spring.datasource.password=

spring.jpa.show-sql=true

此时当运行单元测试时,Spring将默认加载此文件做为应用的配置文件,进行在单元测试中启用了h2数据库:

@SpringBootTest
class StudentRepositoryTest {
    @Autowired
    StudentRepository studentRepository;

    /**
     * 由于本机并没有安装mysql(即使安装了,也没有exampleDb数据库)
     * 所以如果本方法被成功执行,则说明当前单元测试连接的为H2数据库
     */
    @Test
    void findAll() {
        this.studentRepository.findAll();
    }
}

4.2 自定义测试配置文件

如果我们仅仅是需要在部分的单元测试中启用H2数据库而非全部测试,那么单独定义一个测试配置文件是个不错的选择。比如在src/test/resources建立student.properites,专门用于测试对student数据表的操作:

注意:src/main/resources建立也是相同的。本节在起初位置已经介绍了:Spring进行单元测试在查找某个配置位置时会优先查找src/test/resources文件夹,如果找不到则还会查找src/main/resources文件夹。

url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1
# 若使用注释的值设置url,则在StudentJpaConfigTest测试时将尝试连接mysql数据库,进而会发生异常。
# 这说明了此设置文件在单元测试时生效了
# url=jdbc:mysql://localhost:3306/exampleDb
username=root
password=

接下下让我们创建一个使用@Configuration注解的类,并将其设置为:搜索student.properites文件并自动装配至Environment变量。并使用@PropertySource来绑定配置文件:

@Configuration
@PropertySource("classpath:student.properties")
public class StudentJpaConfig {

    @Autowired
    Environment environment;

    @Bean
    DataSource dataSource() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.url(this.environment.getProperty("url"));
        dataSourceBuilder.username(this.environment.getProperty("username"));
        dataSourceBuilder.password(this.environment.getProperty("password"));
        return dataSourceBuilder.build();
    }
}

DataSource bean表示使用当前方法的返回值来替找Spring中的默认DataSource。此时若StudentJpaConfig生效,则应用的数据源将切换为dataSource()方法的返回值,即H2内存数据库。

5. 验证

最后,在我们想启用内存数据库的单元测试上引入该配置类即实现指定某个单元测试连接的数据库为H2数据库的目的:

@SpringBootTest(classes★ = {
        SpringJpaTestInMemoryDatabaseApplication.class★,
        StudentJpaConfig.class★
})
class StudentJpaConfigTest {
    @Autowired
    StudentRepository studentRepository;

    @Test
    void dataSource() {
        this.studentRepository.findAll();
    }
}

运行测试:

2020-09-02 11:02:23.047 INFO 799 --- [ task-1] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table student (id bigint not null, name varchar(255), primary key (id))
Hibernate: select student0_.id as id1_0_, student0_.name as name2_0_ from student student0_

日志输出了当前的方言为H2Dialect以表明当前单元测试使用了H2数据库;日志同时输出了创建数据表以及查询数据表的SQL语句表时数据操作正常。

6. 总结

在单元测试的很多时候,一个体积小、速度快的数据库完全能够满足测试需求(比如我们新建某个数据仓库后,往往需要测试一个findAll方法是否发生异常),所以应该为这些测试应用速度更快、体积更小的H2内存数据库。

但同时由于H2内存数据库在一些细节上仍然与生产环境下的数据库有所差距,所以涉及到一些细节时,仍然需要生产环境下的数据库。这就要求:部分单元测试用生产环境数据库,而另外部分测试则仅需要H2数据库即可。

本文对单元测试中如何启用H2内存数据库进行介绍。