Статьи

Модульное тестирование базы данных с помощью DBUnit, Spring и TestNG


Мне очень нравится Spring, поэтому я стараюсь использовать его возможности в полной мере.
Однако в некоторых темных уголках его философии я склонен не соглашаться с некоторыми из его предположений. Одним из таких предположений является то, как должно работать тестирование базы данных. В этой статье я объясню, как настроить ваши проекты, чтобы
Spring Test и
DBUnit играли вместе в среде нескольких разработчиков.

контекст

Моя основная потребность в том, чтобы иметь возможность тестировать некоторые сложные запросы: перед интеграционными тестами я должен проверить эти запросы, чтобы получить правильные результаты. Это не модульные тесты
сами по себе, но давайте их усвоим как таковые. Чтобы добиться этого, я использую некоторое время фреймворк с именем DBUnit. Хотя не поддерживается с конца 2010 года, я еще не нашел замену (будь моим гостем для предложений). У меня также есть некоторые ограничения:

  • Я хочу использовать TestNG для всех моих тестовых классов, чтобы новые разработчики не думали о том, какую тестовую среду использовать
  • Я хочу иметь возможность использовать Spring Test, чтобы я мог внедрить свои тестовые зависимости непосредственно в тестовый класс
  • Я хочу иметь возможность видеть состояние базы данных в конце любого моего теста, так что, если что-то пойдет не так, я могу выполнить свои собственные запросы, чтобы выяснить, почему
  • Я хочу, чтобы каждый разработчик имел свой собственный экземпляр / схему изолированной базы данных

Учитывая последний момент, наша организация позволила нам воспользоваться единой схемой Oracle для каждого разработчика для этих «модульных тестов».

Базовая настройка

Spring предоставляет готовый класс AbstractTestNGSpringContextTests . В свою очередь, это означает, что мы можем применять аннотации TestNG, а также @Autowired к дочерним классам. Это также означает, что у нас есть доступ к лежащему в основе applicationContext, но я предпочитаю этого не делать (и в любом случае это не нужно). Структура такого теста будет выглядеть так:

@ContextConfiguration(location = "classpath:persistence-beans.xml")
public class MyDaoTest extends AbstractTestNGSpringContextTests {

    @Autowired
    private MyDao myDao;

    @Test
    public void whenXYZThenTUV() {
        ...
    }
}

Читатели, знакомые с Spring и TestNG, не должны удивляться здесь.

Ввод в DBunit


DbUnit — это расширение JUnit, предназначенное для управляемых базой данных проектов, которое, помимо прочего, переводит вашу базу данных в известное состояние между тестовыми запусками.
[…] DbUnit имеет возможность экспортировать и импортировать данные вашей базы данных в и из наборов данных XML. Начиная с версии 2.0, DbUnit также может работать с очень большими наборами данных при использовании в потоковом режиме. DbUnit также может помочь вам проверить, что данные вашей базы данных соответствуют ожидаемому набору значений.

DBunit, являющийся расширением JUnit, должен расширить предоставленный родительский класс org.dbunit.DBTestCase. В моем контексте я должен переопределить некоторые операции настройки и демонтажа, чтобы использовать иерархию наследования Spring. К счастью, разработчики DBUnit обдумали это и предложили соответствующую документацию . Среди различных доступных стратегий мои вкусы склонны к операциям CLEAN_INSERT и NONE соответственно при настройке и демонтаже. Таким образом, я могу проверить состояние базы данных напрямую, если мой тест не пройден. Это обновляет мой тестовый класс следующим образом:

@ContextConfiguration(locations = {"classpath:persistence-beans.xml", "classpath:test-beans.xml"})
public class MyDaoTest extends AbstractTestNGSpringContextTests {

    @Autowired
    private MyDao myDao;

    @Autowired
    private IDatabaseTester databaseTester;

        @BeforeMethod
        protected void setUp() throws Exception {

            // Get the XML and set it on the databaseTester
            // Optional: get the DTD and set it on the databaseTester

            databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
            databaseTester.setTearDownOperation(DatabaseOperation.NONE);
            databaseTester.onSetup();
        }

        @Test
        public void whenXYZThenTUV() {
            ...
    }
}

Конфигурация для пользователя с Spring

Конечно, нам нужен конкретный конфигурационный файл Spring для внедрения databaseTester. Как пример, вот один:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

        <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="location" value="${user.name}.database.properties" />
        </bean>

        <bean name="dataSource" class="org.springframework.jdbc.datasource.SingleConnectionDataSource">
             <property name="driverClass" value="oracle.jdbc.driver" />
             <property name="username" value="${db.username}" />
             <property name="password" value="${db.password}" />
             <property name="url" value="jdbc:oracle:thin:@<server>:<port>/${db.schema}" />
        </bean>

        <bean name="databaseTester" class="org.dbunit.DataSourceDatabaseTester">
            <constructor-arg ref="dataSource" />
        </bean>
</beans>

Тем не менее, есть больше, чем кажется на первый взгляд. Обратите внимание, что databaseTester должен быть снабжен источником данных. Поскольку требуется наличие базы данных на разработчика, в основном есть два варианта: либо использовать базу данных в памяти, либо использовать ту же базу данных, что и в рабочей среде, и предоставить одну такую ​​схему базы данных для разработчика. Я склоняюсь к последнему решению (когда это возможно), так как оно имеет тенденцию уменьшать различия между средой тестирования и производственной средой. Таким образом, чтобы каждый разработчик использовал свою собственную схему, я использую возможность Spring заменять системные свойства Java во время выполнения: каждый разработчик характеризуется своим именем user.name. Затем я настраиваю PlaceholderConfigurer, который ищет файл {user.name} .database.properties, который будет выглядеть так:

db.username=myusername1
db.password=mypassword1
db.schema=myschema1

Это позволило мне достичь цели каждого разработчика, использующего свой собственный экземпляр Oracle. Если вы хотите использовать эту стратегию, не забудьте указать конкретные свойства database.properties для сервера непрерывной интеграции.

А?

Наконец, вся цепочка тестирования настроена до уровня базы данных. Тем не менее, когда запускается предыдущий тест, все в порядке (или нет), но при проверке базы данных он выглядит нетронутым. Как ни странно, если вы загрузили некоторый набор данных XML и подтвердили его во время теста, он ведет себя соответствующим образом: это несет в себе все признаки проблемы транзакции. На самом деле, если вы внимательно посмотрите на документацию Spring, все станет ясно. Видение Spring заключается в том, что при выполнении тестов базу данных следует оставить нетронутой, что полностью противоречит принципам DBUnit. Это достигается простым откатом всех изменений в конце теста по умолчанию. Чтобы изменить это поведение, нужно только аннотировать тестовый класс с помощью @TransactionConfiguration (defaultRollback = false). Обратите внимание, это неМы не можем указать конкретные методы, которые не должны влиять на состояние базы данных в каждом конкретном случае, с аннотацией @Rollback. Тестовый класс становится:

@ContextConfiguration(locations = {classpath:persistence-beans.xml", "classpath:test-beans.xml"})
@TransactionConfiguration(defaultRollback=false)
public class MyDaoTest extends AbstractTestNGSpringContextTests {

    @Autowired
    private MyDao myDao;

    @Autowired
    private IDatabaseTester databaseTester;

	@BeforeMethod
	protected void setUp() throws Exception {

		// Get the XML and set it on the databaseTester
		// Optional: get the DTD and set it on the databaseTester

        databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
        databaseTester.setTearDownOperation(DatabaseOperation.NONE);
        databaseTester.onSetup();
    }

    @Test
    public void whenXYZThenTUV() {
	...
    }
}

Вывод

Хотя взгляды Spring и DBUnit на тестирование базы данных противоположны, гибкость конфигурации Spring позволяет нам сделать ее соответствующей нашим потребностям (и пользе от DI). Конечно, возможны и другие улучшения: добавление общего кода в родительский тестовый класс и т. Д. Чтобы пойти дальше: