Chapter 4: 测试关系型数据库

Spring Test Framework提供了对JDBC的支持,能够让我们很方便对关系型数据库做集成测试。

同时Spring Boot提供了和Flyway集成支持,能够方便的管理开发过程中产生的SQL文件,配合Spring已经提供的工具能够更方便地在测试之前初始化数据库以及测试之后清空数据库。

本章节为了方便起见,本章节使用了H2作为测试数据库。

注意:在真实的开发环境中,集成测试用数据库应该和最终的生产数据库保持一致,这是因为不同数据库的对于SQL不是完全相互兼容的,如果不注意这一点,很有可能出现集成测试通过,但是上了生产环境却报错的问题。

因为是集成测试,所以我们使用了maven-failsafe-plugin来跑,它和maven-surefire-plugin的差别在于,maven-failsafe-plugin只会搜索*IT.java来跑测试,而maven-surefire-plugin只会搜索*Test.java来跑测试。

如果想要在maven打包的时候跳过集成测试,只需要mvn clean install -DskipITs

被测试类

先介绍一下被测试的类。

Foo.java

  1. public class Foo {
  2. private String name;
  3. public String getName() {
  4. return name;
  5. }
  6. public void setName(String name) {
  7. this.name = name;
  8. }
  9. }

FooRepositoryImpl.java

  1. @Repository
  2. public class FooRepositoryImpl implements FooRepository {
  3. private JdbcTemplate jdbcTemplate;
  4. @Override
  5. public void save(Foo foo) {
  6. jdbcTemplate.update("INSERT INTO FOO(name) VALUES (?)", foo.getName());
  7. }
  8. @Override
  9. public void delete(String name) {
  10. jdbcTemplate.update("DELETE FROM FOO WHERE NAME = ?", name);
  11. }
  12. @Autowired
  13. public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
  14. this.jdbcTemplate = jdbcTemplate;
  15. }
  16. }

例子1:不使用Spring Testing提供的工具

Spring_1_IT_Configuration.java

  1. @Configuration
  2. @ComponentScan(basePackageClasses = FooRepository.class)
  3. public class Spring_1_IT_Configuration {
  4. @Bean(destroyMethod = "shutdown")
  5. public DataSource dataSource() {
  6. return new EmbeddedDatabaseBuilder()
  7. .generateUniqueName(true)
  8. .setType(EmbeddedDatabaseType.H2)
  9. .setScriptEncoding("UTF-8")
  10. .ignoreFailedDrops(true)
  11. .addScript("classpath:me/chanjar/domain/foo-ddl.sql")
  12. .build();
  13. }
  14. @Bean
  15. public JdbcTemplate jdbcTemplate() {
  16. return new JdbcTemplate(dataSource());
  17. }
  18. }

Spring_1_IT_Configuration中,我们定义了一个H2的DataSource Bean,并且构建了JdbcTemplate Bean。

注意看addScript("classpath:me/chanjar/domain/foo-ddl.sql")这句代码,我们让EmbeddedDatabase执行foo-ddl.sql脚本来建表:

  1. CREATE TABLE FOO (
  2. name VARCHAR2(100)
  3. );

Spring_1_IT.java

  1. @ContextConfiguration(classes = Spring_1_IT_Configuration.class)
  2. public class Spring_1_IT extends AbstractTestNGSpringContextTests {
  3. @Autowired
  4. private FooRepository fooRepository;
  5. @Autowired
  6. private JdbcTemplate jdbcTemplate;
  7. @Test
  8. public void testSave() {
  9. Foo foo = new Foo();
  10. foo.setName("Bob");
  11. fooRepository.save(foo);
  12. assertEquals(
  13. jdbcTemplate.queryForObject("SELECT count(*) FROM FOO", Integer.class),
  14. Integer.valueOf(1)
  15. );
  16. }
  17. @Test(dependsOnMethods = "testSave")
  18. public void testDelete() {
  19. assertEquals(
  20. jdbcTemplate.queryForObject("SELECT count(*) FROM FOO", Integer.class),
  21. Integer.valueOf(1)
  22. );
  23. Foo foo = new Foo();
  24. foo.setName("Bob");
  25. fooRepository.save(foo);
  26. fooRepository.delete(foo.getName());
  27. assertEquals(
  28. jdbcTemplate.queryForObject("SELECT count(*) FROM FOO", Integer.class),
  29. Integer.valueOf(0)
  30. );
  31. }
  32. }

在这段测试代码里可以看到,我们分别测试了FooRepositorysavedelete方法,并且利用JdbcTemplate来验证数据库中的结果。

例子2:使用Spring Testing提供的工具

在这个例子里,我们会使用JdbcTestUtils来辅助测试。

Spring_2_IT_Configuration.java

  1. @Configuration
  2. @ComponentScan(basePackageClasses = FooRepository.class)
  3. public class Spring_2_IT_Configuration {
  4. @Bean
  5. public DataSource dataSource() {
  6. EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
  7. .generateUniqueName(true)
  8. .setType(EmbeddedDatabaseType.H2)
  9. .setScriptEncoding("UTF-8")
  10. .ignoreFailedDrops(true)
  11. .addScript("classpath:me/chanjar/domain/foo-ddl.sql")
  12. .build();
  13. return db;
  14. }
  15. @Bean
  16. public JdbcTemplate jdbcTemplate() {
  17. return new JdbcTemplate(dataSource());
  18. }
  19. @Bean
  20. public PlatformTransactionManager transactionManager() {
  21. return new DataSourceTransactionManager(dataSource());
  22. }
  23. }

这里和例子1的区别在于,我们提供了一个PlatformTransactionManager Bean,这是因为在下面的测试代码里的AbstractTransactionalTestNGSpringContextTests需要它。

Spring_2_IT.java

  1. @ContextConfiguration(classes = Spring_2_IT_Configuration.class)
  2. public class Spring_2_IT extends AbstractTransactionalTestNGSpringContextTests {
  3. @Autowired
  4. private FooRepository fooRepository;
  5. @Test
  6. public void testSave() {
  7. Foo foo = new Foo();
  8. foo.setName("Bob");
  9. fooRepository.save(foo);
  10. assertEquals(countRowsInTable("FOO"), 1);
  11. countRowsInTableWhere("FOO", "name = 'Bob'");
  12. }
  13. @Test(dependsOnMethods = "testSave")
  14. public void testDelete() {
  15. assertEquals(countRowsInTable("FOO"), 0);
  16. Foo foo = new Foo();
  17. foo.setName("Bob");
  18. fooRepository.save(foo);
  19. fooRepository.delete(foo.getName());
  20. assertEquals(countRowsInTable("FOO"), 0);
  21. }
  22. }

在这里我们使用countRowsInTable("FOO")来验证数据库结果,这个方法是AbstractTransactionalTestNGSpringContextTestsJdbcTestUtils的代理。

而且要注意的是,每个测试方法在执行完毕后,会自动rollback,所以在testDelete的第一行里,我们assertEquals(countRowsInTable("FOO"), 0),这一点和例子1里是不同的。

更多关于Spring Testing Framework与Transaction相关的信息,可以见Spring官方文档 Transaction management

例子3:使用Spring Boot

Boot_1_IT.java

  1. @SpringBootTest
  2. @SpringBootApplication(scanBasePackageClasses = FooRepository.class)
  3. public class Boot_1_IT extends AbstractTransactionalTestNGSpringContextTests {
  4. @Autowired
  5. private FooRepository fooRepository;
  6. @Test
  7. public void testSave() {
  8. Foo foo = new Foo();
  9. foo.setName("Bob");
  10. fooRepository.save(foo);
  11. assertEquals(countRowsInTable("FOO"), 1);
  12. countRowsInTableWhere("FOO", "name = 'Bob'");
  13. }
  14. @Test(dependsOnMethods = "testSave")
  15. public void testDelete() {
  16. assertEquals(countRowsInTable("FOO"), 0);
  17. Foo foo = new Foo();
  18. foo.setName("Bob");
  19. fooRepository.save(foo);
  20. fooRepository.delete(foo.getName());
  21. assertEquals(countRowsInTable("FOO"), 0);
  22. }
  23. @AfterTest
  24. public void cleanDb() {
  25. flyway.clean();
  26. }
  27. }

因为使用了Spring Boot来做集成测试,得益于其AutoConfiguration机制,不需要自己构建DataSourceJdbcTemplatePlatformTransactionManager的Bean。

并且因为我们已经将flyway-core添加到了maven依赖中,Spring Boot会利用flyway来帮助我们初始化数据库,我们需要做的仅仅是将sql文件放到classpath的db/migration目录下:

V1.0.0__foo-ddl.sql:

  1. CREATE TABLE FOO (
  2. name VARCHAR2(100)
  3. );

而且在测试最后,我们利用flyway清空了数据库:

  1. @AfterTest
  2. public void cleanDb() {
  3. flyway.clean();
  4. }

使用flyway有很多好处:

  1. 每个sql文件名都规定了版本号
  2. flyway按照版本号顺序执行
  3. 在开发期间,只需要将sql文件放到db/migration目录下就可以了,不需要写类似EmbeddedDatabaseBuilder.addScript()这样的代码
  4. 基于以上三点,就能够将数据库初始化SQL语句也纳入到集成测试中来,保证代码配套的SQL语句的正确性
  5. 可以帮助你清空数据库,这在你使用非内存数据库的时候非常有用,因为不管测试前还是测试后,你都需要一个干净的数据库

参考文档

本章节涉及到的Spring Testing Framework JDBC、SQL相关的工具:

和flyway相关的: