Интеграционное тестирование и Spring JDBC

Почти все примеры в статьях и о 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));
    }
}

Скачать код примера

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *