Почти все примеры в статьях и о JDBC и о Spring JDBC были написаны по одному шаблону — подготавливаем структуру базы данных, наполняем её тестовыми данными, исполняем какой-то код на тестовых данных, очищаем базу данных. В статье о признаках хорошего теста я писал, что так устроен практически любой тест: подготовка тестовой среды, выполнение теста, очистка тестовой среды.
Конечно, примеры использования Spring JDBC из моих статей нехарактерны для кода, встречающегося в дикой природе, но зато интеграционные тесты обычно так и выглядят. Spring JDBC конечно спасает, когда тесты исполняются на встроенных базах данных, но он не может выполнять скрипты для внешних баз данных и управлять их жизненным циклом.
С другой стороны, Spring JDBC предоставляет утилиты, упрощающие разработку интеграционных тестов для кода, работающего с базами данных.
Подготовка
За основу я взял код из примера «Запросы в Spring JDBC» и немного его изменил. В первую очередь, встроенная база H2 заменена на PostgreSQL:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <postgresql.version>9.4-1206-jdbc42</postgresql.version> </properties>
<dependencies> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> </dependencies>
<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 сервере:
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 скрипты в указанном порядке. Звучит просто, но реально это очень мощный механизм для работы с данными. Его можно применять как в тестах, так и в каких-либо пакетных операциях над базой и вообще по любому поводу.
Я с его помощью буду инициализировать таблицы для интеграционного теста:
@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
Второй вспомогательный класс имеет название, намекающее, что он ориентирован только на тесты. В основном он ориентирован на очищение базы данных после теста.
@After public void tearDown() { JdbcTestUtils.dropTables(jdbcTemplate, "customers"); }
В данном случае он удаляет таблицу customers, создаваемую скриптами в методе setUp(). Размещение инициализации/деинициализации в методах @Before/@After гарантирует, что каждый тест класса получит одинаковый набор данных, заданный скриптами.
Автоматическое выполнение скриптов
Если внимательно присмотреться к коду setUp() выше, видно, что вообщем-то для разных тестов он будут отличаться только списком скриптов, а в остальном будет тот же самый. Spring JDBC позволяет не повторяться и не копипастить ResourceDatabasePopulator из теста в тест, а указывать набор скриптов в качестве метаданных теста или тестового класса.
@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 методе.
@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)); } }