今天我想谈谈测试,因为测试覆盖的代码越多,就被认为越好、越可靠。我们不谈单元测试,谈数据库的集成测试。单元测试和集成测试之间到底有什么区别? 模块化(单元)是在单个模块、方法或类的级别上测试程序,也就是说,测试快速而简单,影响功能中最可分割的部分。它们也被称为“每种方法一个测试”。集成的速度较慢且较重,并且可以由多个模块和附加功能组成。 为什么要进行dao(数据访问对象)层集成测试?因为要测试对数据库进行查询的方法,我们需要在 RAM 中建立一个单独的数据库来替换主数据库。我们的想法是,我们创建所需的表,用测试数据填充它们并检查存储库类方法的正确性(毕竟,我们知道在给定情况下最终结果应该是什么)。那么,让我们开始吧。关于连接数据库的主题早已被广泛讨论,因此今天我不想详细讨论这个问题,我们将只考虑程序中我们感兴趣的部分。默认情况下,我们将从我们的应用程序基于 Spring Boot 开始,对于 Spring JDBC dao 层(为了更清楚),我们的主数据库是 MySQL,我们将使用 MariaDB 替换它(它们是最大兼容的,并且因此,MySQL 脚本永远不会与 MariaDB 方言发生冲突,就像 H2 一样)。我们还将有条件地假设我们的程序使用 Liquibase 来管理和应用对数据库模式的更改,因此,所有应用的脚本都存储在我们的应用程序中。
项目结构
仅显示受影响的部分: 是的,今天我们将创建机器人)) 表的脚本,我们今天将测试的方法(create_table_robots.sql):CREATE TABLE `robots`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` CHAR(255) CHARACTER SET utf8 NOT NULL,
`cpu` CHAR(255) CHARACTER SET utf8 NOT NULL,
`producer` CHAR(255) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
代表该表的实体:
@Builder
@Data
public class Robot {
private Long id;
private String name;
private String cpu;
private String producer;
}
测试存储库的接口:
public interface RobotDAO {
Robot findById(Long id);
Robot create(Robot robot);
List<Robot> findAll();
Robot update(Robot robot);
void delete(Long id);
}
实际上,这里是标准的 CRUD 操作,没有外来的,所以我们不会考虑所有方法的实现(好吧,这不会让任何人感到惊讶),而是一些方法 - 为了更简洁:
@Repository
@AllArgsConstructor
public class RobotDAOImpl implements RobotDAO {
private static final String FIND_BY_ID = "SELECT id, name, cpu, producer FROM robots WHERE id = ?";
private static final String UPDATE_BY_ID = "UPDATE robots SET name = ?, cpu = ?, producer = ? WHERE id = ?";
@Autowired
private final JdbcTemplate jdbcTemplate;
@Override
public Robot findById(Long id) {
return jdbcTemplate.queryForObject(FIND_BY_ID, robotMapper(), id);
}
@Override
public Robot update(Robot robot) {
jdbcTemplate.update(UPDATE_BY_ID,
robot.getName(),
robot.getCpu(),
robot.getProducer(),
robot.getId());
return robot;
}
private RowMapper<Robot> robotMapper() {
return (rs, rowNum) ->
Robot.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.cpu(rs.getString("cpu"))
.producer(rs.getString("producer"))
.build();
}
让我们稍微离题一下,看看我们的依赖项发生了什么(仅提供了用于应用程序演示部分的依赖项):
<dependencies>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.craftercms.mariaDB4j</groupId>
<artifactId>mariaDB4j-springboot</artifactId>
<version>2.4.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.1.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>
4 - MariaDb 数据库本身的依赖项 10 - 连接 SpringBoot 的依赖项 16 - Lombok (嗯,我想每个人都知道这是什么类型的库) 22 - 测试的启动器(嵌入了我们需要的 JUnit) 28 - 启动器使用 springJdbc 让我们看一下 Spring 容器,其中包含我们测试所需的 bean(特别是 MariaDB 创建 bean):
@Configuration
public class TestConfigDB {
@Bean
public MariaDB4jSpringService mariaDB4jSpringService() {
return new MariaDB4jSpringService();
}
@Bean
public DataSource dataSource(MariaDB4jSpringService mariaDB4jSpringService) {
try {
mariaDB4jSpringService.getDB().createDB("testDB");
} catch (ManagedProcessException e) {
e.printStackTrace();
}
DBConfigurationBuilder config = mariaDB4jSpringService.getConfiguration();
return DataSourceBuilder
.create()
.username("root")
.password("root")
.url(config.getURL("testDB"))
.driverClassName("org.mariadb.jdbc.Driver")
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
5 - 提升 MariaDB 的主要组件(适用于基于 Spring 框架的应用程序) 10 - 定义数据库 bean 12 - 设置创建的数据库的名称 17 - 为我们的案例提取配置 19 - 使用 Builder 模式构建数据库(对该模式的一个很好的概述)最后,最值得关注的是用于与所引发的数据库进行通信的 JdbcTemplate bean。我们的想法是,我们将有一个用于Tao测试的主类,所有Dao测试类都将从该类继承,其任务包括:
- 启动主数据库中使用的一些脚本(用于创建表、更改列等的脚本);
- 启动用测试数据填充表格的测试脚本;
- 删除表。
@SpringBootTest(classes = TestConfigDB.class)
public abstract class DataBaseIT {
@Autowired
private JdbcTemplate jdbcTemplate;
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void fillDataBase(String[] initList) {
for (String x : initList) {
try {
jdbcTemplate.update(IOUtils.resourceToString("/db.migrations/" + x, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void cleanDataBase() {
getJdbcTemplate().update("DROP database testDB");
getJdbcTemplate().update("CREATE database testDB");
getJdbcTemplate().update("USE testDB");
}
public void fillTables(String[] fillList) {
for (String x : fillList) {
try {
Stream.of(
IOUtils.resourceToString("/fill_scripts/" + x, StandardCharsets.UTF_8))
.forEach(jdbcTemplate::update);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1 - 使用@SpringBootTest注释我们设置一个测试配置 11 - 作为这个方法中的参数,我们传递我们需要的表的名称,他,作为一个负责任的勤奋工作者,将为我们加载它们(这给了我们机会尽可能多地重用此方法) 21 - 我们使用此方法进行清理,即从数据库中删除所有表(及其数据) 27 - 此方法中的参数是带有测试数据的脚本名称数组将加载用于测试特定方法。我们的脚本包含测试数据:
INSERT INTO robots(name, cpu, producer)
VALUES ('Rex', 'Intel Core i5-9400F', 'Vietnam'),
('Molly', 'AMD Ryzen 7 2700X', 'China'),
('Ross', 'Intel Core i7-9700K', 'Malaysia')
现在我们今天聚集在一起。
道测试班
@RunWith(SpringRunner.class)
public class RobotDataBaseIT extends DataBaseIT {
private static RobotDAO countryDAO;
@Before
public void fillData() {
fillDataBase(new String[]{
"create_table_robots.sql"
});
countryDAO = new RobotDAOImpl(getJdbcTemplate());
}
@After
public void clean() {
cleanDataBase();
}
private RowMapper<Robot> robotMapper() {
return (rs, rowNum) ->
Robot.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.cpu(rs.getString("cpu"))
.producer(rs.getString("producer"))
.build();
}
2 - 我们从测试的主类继承 4 - 我们测试的存储库 7 - 将在每次测试之前启动的方法 8 - 我们使用父类方法加载必要的表 11 - 我们初始化 dao 15 - 一个方法它将在每次测试后启动,清理我们的数据库 19 - RowMapper 的实现,类似于 Tai 类 我们使用 @Before 和 @After,它们在一个测试方法之前和之后使用,但我们可以使用一些允许我们使用的库使用与此类的执行测试的开始和结束相关的注释。例如,这将显着加快测试速度,因为每次都必须创建表并完全删除,每个类一次。但我们不这样做。你为什么问?如果其中一种方法更改了表的结构怎么办?例如,删除一列。在这种情况下,其余方法可能会失败或必须按预期响应(例如,创建后列)。我们不得不承认,这给我们带来了测试之间不必要的联系(依赖),这对我们来说没有任何用处。但我离题了,让我们继续……
测试 findById 方法
@Test
public void findByIdTest() {
fillTables(new String[]{"fill_table_robots.sql"});
Long id = getJdbcTemplate().queryForObject("SELECT id FROM robots WHERE name = 'Molly'", Long.class);
Robot robot = countryDAO.findById(id);
assertThat(robot).isNotNull();
assertThat(robot.getId()).isEqualTo(id);
assertThat(robot.getName()).isEqualTo("Molly");
assertThat(robot.getCpu()).isEqualTo("AMD Ryzen 7 2700X");
assertThat(robot.getProducer()).isEqualTo("China");
}
3 - 用测试数据填充表格 5 - 获取我们需要的实体的 ID 6 - 使用正在测试的方法 8...12 - 将接收到的数据与预期数据进行比较
更新方法测试
@Test
public void updateTest() {
fillTables(new String[]{"fill_table_robots.sql"});
Long robotId = getJdbcTemplate().queryForObject("SELECT id FROM robots WHERE name = 'Rex'", Long.class);
Robot updateRobot = Robot.builder()
.id(robotId)
.name("Aslan")
.cpu("Intel Core i5-3470")
.producer("Narnia")
.build();
Robot responseRobot = countryDAO.update(updateRobot);
Robot updatedRobot = getJdbcTemplate().queryForObject(
"SELECT id, name, cpu, producer FROM robots WHERE id = ?",
robotMapper(),
robotId);
assertThat(updatedRobot).isNotNull();
assertThat(updateRobot.getName()).isEqualTo(responseRobot.getName());
assertThat(updateRobot.getName()).isEqualTo(updatedRobot.getName());
assertThat(updateRobot.getCpu()).isEqualTo(responseRobot.getCpu());
assertThat(updateRobot.getCpu()).isEqualTo(updatedRobot.getCpu());
assertThat(updateRobot.getProducer()).isEqualTo(responseRobot.getProducer());
assertThat(updateRobot.getProducer()).isEqualTo(updatedRobot.getProducer());
assertThat(responseRobot.getId()).isEqualTo(updatedRobot.getId());
assertThat(updateRobot.getId()).isEqualTo(updatedRobot.getId());
}
3 - 用测试数据填充表 5 - 获取正在更新的实体的 ID 7 - 构建更新的实体 14 - 使用正在测试的方法 15 - 获取更新的实体进行验证 20...28 - 将接收到的数据与测试更新方法与创建类似。至少对于我来说。您可以随意调整对账:检查永远不会太多。我还想指出,测试并不能保证功能完整或不存在错误。测试仅确保程序(其片段)的实际结果与预期结果相对应。在这种情况下,仅检查那些为其编写测试的部分。
让我们启动一个带有测试的课程......
胜利))我们去泡茶,吃饼干:我们应得的))有用的链接
- 一篇关于该技术堆栈未经审查的部分的好文章。
- 关于 Maven、Spring、MySQL、Hibernate 的有趣系列文章。
- 让我们回顾一下单元测试。
- 一个有趣的集成测试示例,但使用 PostgreSQL 替代。
- 许多人,不仅在测试中,使用 MariaDB 而不是 MySql。
*史诗般的星球大战音乐*
GO TO FULL VERSION