Почти все примеры в статьях и о JDBC и о Spring JDBC были написаны по одному шаблону — подготавливаем структуру базы данных, наполняем её тестовыми данными, исполняем какой-то код на тестовых данных, очищаем базу данных. В статье о признаках хорошего теста я писал, что так устроен практически любой тест: подготовка тестовой среды, выполнение теста, очистка тестовой среды.
Конечно, примеры использования Spring JDBC из моих статей нехарактерны для кода, встречающегося в дикой природе, но зато интеграционные тесты обычно так и выглядят. Spring JDBC конечно спасает, когда тесты исполняются на встроенных базах данных, но он не может выполнять скрипты для внешних баз данных и управлять их жизненным циклом.
С другой стороны, Spring JDBC предоставляет утилиты, упрощающие разработку интеграционных тестов для кода, работающего с базами данных.
Подготовка
За основу я взял код из примера «Запросы в Spring JDBC» и немного его изменил. В первую очередь, встроенная база H2 заменена на PostgreSQL:
1
2
3
4
|
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<postgresql.version>9.4-1206-jdbc42</postgresql.version>
</properties>
|
1
2
3
4
5
6
7
|
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
</dependencies>
|
1
2
3
4
5
6
|
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.postgresql.Driver"/>
<property name="url" value="jdbc:postgresql://127.0.0.1/test"/>
<property name="username" value="test"/>
<property name="password" value="test"/>
</bean>
|
Кроме того, я подготовил пустую базу в PostgreSQL сервере:
1
2
3
4
5
6
|
CREATE ROLE test WITH PASSWORD 'test';
ALTER ROLE test WITH LOGIN;
CREATE DATABASE test OWNER test;
GRANT CREATE ON DATABASE test TO test;
|
SQL скрипты, которые создают структуру базы и наполняют её данными, я разбил на отдельные скрипты создания каждой таблицы и отдельные скрипты для наполнения этих таблиц данными.
Выполнение SQL скриптов.
Класс ResourceDatabasePopulator позволяет выполнять SQL скрипты в указанном порядке. Звучит просто, но реально это очень мощный механизм для работы с данными. Его можно применять как в тестах, так и в каких-либо пакетных операциях над базой и вообще по любому поводу.
Я с его помощью буду инициализировать таблицы для интеграционного теста:
1
2
3
4
5
6
7
8
|
@Before
public void setUp() {
ResourceDatabasePopulator tables = new ResourceDatabasePopulator();
tables.addScript(new ClassPathResource("/customers-table.sql"));
tables.addScript(new ClassPathResource("/customers-data.sql"));
DatabasePopulatorUtils.execute(tables, dataSource);
}
|
ResourceDatabasePopulator принимает SQL скрипты как экземпляры класса Resource, который скрывает истинный источник данных и позволяет использовать файлы из classpath, файлы из файловой системы, напрямую из интернета, байтовые потоки и т.д. Скрипты выполняются в порядке выполнения. Кроме задания списка скриптов можно задать условия их выполнения: разделитель запросов, символы комментирования, игнорирование ошибок и прочая. Готовый, сконфигурированный DatabasePopulator можно исполнить на базе данных с помощью статического метода execute() класса DatabasePopulatorUtils. Метод execute() ожидает получить объект DataSource, а не JdbcTemplate в качестве ссылки на базу данных.
JdbcTestUtils
Второй вспомогательный класс имеет название, намекающее, что он ориентирован только на тесты. В основном он ориентирован на очищение базы данных после теста.
1
2
3
4
|
@After
public void tearDown() {
JdbcTestUtils.dropTables(jdbcTemplate, "customers");
}
|
В данном случае он удаляет таблицу customers, создаваемую скриптами в методе setUp(). Размещение инициализации/деинициализации в методах @Before/@After гарантирует, что каждый тест класса получит одинаковый набор данных, заданный скриптами.
Автоматическое выполнение скриптов
Если внимательно присмотреться к коду setUp() выше, видно, что вообщем-то для разных тестов он будут отличаться только списком скриптов, а в остальном будет тот же самый. Spring JDBC позволяет не повторяться и не копипастить ResourceDatabasePopulator из теста в тест, а указывать набор скриптов в качестве метаданных теста или тестового класса.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@ContextConfiguration("/applicationContext.xml")
@SqlGroup({
@Sql("/skus-table.sql"),
@Sql("/skus-data.sql")
})
@Sql( scripts = "/skus-delete.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@RunWith(SpringJUnit4ClassRunner.class)
public class SkuRepositoryJdbcIT {
@Test
public void testCreate() {
JdbcTestUtils.deleteFromTables(jdbcTemplate, "skus");
Sku expected = new Sku();
expected.setId(1);
expected.setDescription("NEWBIE");
testedObject.add(expected);
assertThat(JdbcTestUtils.countRowsInTable(jdbcTemplate, "skus"), is(1));
}
}
|
Аннотации @SqlGroup и @Sql говорят Spring test framework, что перед запуском каждого теста (поведение по умолчанию) или после завершения каждого теста (если указан параметр executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) необходимо выполнить скрипт/группу скриптов. В примере выше перед запуском каждого теста выполняются два скрипта, которые создают таблицу и наполняют её данными. А после выполнения тестов запускается sql скрипт, удаляющий таблицу.
И опять JdbcTestUtils
В примере выше в тестовом методе используются два новых метода JdbcTestUtils. Вначале метод JdbcTestUtils.deleteFromTables() очищает таблицы, не удаляя их. Затем, после того как операции над базой выполнены, метод JdbcTestUtils.countRowsInTable() подсчитывает текущее количество строк в таблице. Результат подсчёта сравнивается с эталоном.
Оба метода имеют компаньонов, JdbcTestUtils.deleteFromTableWhere() и JdbcTestUtils.countRowsInTableWhere(), которые позволяют указать условия для выполнения операции. Кроме того, метод JdbcTestUtils.deleteFromTables() принимает несколько аргументов, позволяя очистить несколько таблиц сразу. Таким же поведением обладает и JdbcTestUtils.dropTables() из примера с @Before/@After, который так же способен удалить несколько таблиц.
Всё вместе.
Всё перечисленное выше можно комбинировать. Например таблицы можно создавать скриптом, а удалять в @After методе.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
@ContextConfiguration("/applicationContext.xml")
@SqlGroup({
@Sql("/customers-table.sql"),
@Sql("/customers-data.sql"),
@Sql("/orders-table.sql"),
@Sql("/orders-data.sql"),
})
@RunWith(SpringJUnit4ClassRunner.class)
public class OrderRepositoryJdbcIT {
@Inject
private JdbcTemplate jdbcTemplate;
@Inject
private OrderRepositoryJdbc testedObject;
@After
public void tearDown() {
JdbcTestUtils.dropTables(jdbcTemplate, "orders", "customers");
}
@Test
public void testGet() {
Order actual = testedObject.get(100);
assertThat(actual.getId(), is(100));
assertThat(actual.getCustomer().getId(), is(100));
assertThat(actual.getCustomer().getEmail(), is("TEST"));
}
@Test
public void testAll() {
assertThat(testedObject.all().size(), is(7));
}
@Test
public void testOrderCount() {
Customer c = new Customer();
c.setId(2);
Number actual = testedObject.ordersForCustomer(c);
int expected = JdbcTestUtils.countRowsInTableWhere(jdbcTemplate, "orders", "customer_id=2");
Assert.assertThat(actual, is(expected));
}
}
|
Код примера доступен на github.