Основы тестирования с помощью Java фреймворка DBUnit
Denys BezsmertnyiОпубликовано 20.10.2008 в Разработка
DBUnit – Java open source фреймворк (расширение JUnit), который приводит базу данных в определенное состояние между вызовами тестов.
Если вам необходимо перед вызовом каждого теста загружать данные в базу данных, а после вызова тестируемой функциональности, которая модифицирует данные в базе данных, проверять правильность изменений – DBUnit будет хорошим выбором для решения подобных задач.
Как использовать DBUnit
Предположим, что нам необходимо протестировать метод, который запускается планировщиком задач в начале нового года на одном из предприятий:
void updateAllSalaries()
Логика данного метода рассчитывает новое значение зарплаты каждого сотрудника предприятия, основываясь на различных данных (к примеру, опыт работы, отзывы коллег, выполнение задач в поставленные сроки), которые запрашиваются из различных таблиц, и изменят зарплату сотрудников, сохраняя новое значение в таблице EMPLOYEE, которая имеет следующую структуру:

Для хранения данных, используемых тестируемым методом для анализа, в целях вычисления нового значения заработной платы, добавим таблицу EMPLOYEE_PROGRESS, которая в столбце PROGRESS будет содержать количество выполненых задач в поставленные сроки в процентном выражении:

Для простоты, тестируемый метод будет действовать следующим образом при вычислении нового значения зарплаты:
Если прогресс больше 50%, тогда
Новая зарплата = SALARY + (SALARY * Progress) / 100.
Иначе
Новая зарплата = SALARY - (SALARY * Progress) / 100.
Один из самых простых способов создать DBUnit-тест – расширить класс org.dbunit.DBTestCase, который является непрямым наследником класса junit.framework.TestCase:
package com.company.test;
import org.dbunit.DBTestCase;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
public class SimpleDBTestCase extends DBTestCase {
@Override
protected IDataSet getDataSet() throws Exception {
return new FlatXmlDataSet(getClass().getResourceAsStream(
"/data/SalaryTestDataSet.xml"));
}
}
Переопределенный метод getDataSet возвращает объект IDataSet, который был создан на основании источника, данные которого будут загружены в базу данных перед стартом теста. В данном случае загружаемые данные находятся в XML файле SalaryTestDataSet.xml и выглядят следующим образом:
<dataset>
<employee ID="1" NAME="Sara" SALARY="465.00"/>
<employee ID="2" NAME="David" SALARY="789.00"/>
<employee ID="3" NAME="Mariya" SALARY="711.00"/>
<employee_progress ID="1" EMPLOYEE_ID="1" PROGRESS="90"/>
<employee_progress ID="2" EMPLOYEE_ID="2" PROGRESS="95"/>
<employee_progress ID="3" EMPLOYEE_ID="3" PROGRESS="48"/>
</dataset>
Каждый XML-тег в данном файле представляет строку таблицы базы данных. Название тега – это имя таблицы, а атрибуты и их значения представляют имена столбцов и загружаемые в них данные.
Также нам необходимо добавить информацию о соединении с базой данных.
Для простоты я добавляю эту информацию в конструкторе через системные свойства, хотя есть и другие более изящные способы.
public SimpleDBTestCase(String testName) throws Exception {
super(testName);
System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_DRIVER_CLASS,
"oracle.jdbc.driver.OracleDriver");
System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_CONNECTION_URL,
"jdbc:oracle:thin:@localhost:1521:XE");
System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_USERNAME,
"login");
System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_PASSWORD,
"password");
System.setProperty(PropertiesBasedJdbcDatabaseTester.DBUNIT_SCHEMA,
"DB_SCHEMA");
}
Добавляем метод для тестирования функциональности:
1. public void testUpdateAllSalaries() throws Exception {
2. AccountService accountService = new AccountService();
3. accountService.updateAllSalaries();
4. IDataSet databaseDataSet = getConnection().createDataSet();
5. ITable actualTable = databaseDataSet.getTable("EMPLOYEE");
6. IDataSet expectedDataSet = new FlatXmlDataSet(getClass().getResourceAsStream("/dataSalaryTestDataSet_Expected.xml"));
7. ITable expectedTable = expectedDataSet.getTable("EMPLOYEE");
8. ITable filteredActualTable = DefaultColumnFilter.includedColumnsTable(actualTable, expectedTable.getTableMetaData().getColumns());
9. Assertion.assertEquals(expectedTable, filteredActualTable);
10. }
В строке 2 и 3 мы вызываем функциональность, которую необходимо протестировать.
Как мы помним, необходимые данные для тестирования этой функциональности уже были загружены в базу данных.
В строке 4 мы получаем доступ к базе данных и создаем объект типа IDataSet, который представляет все таблицы из базы данных. А в строке 5 получаем интересующую нас таблицу.
В строке 6 мы загружаем из проверочного источника данных (в данном случае тоже XML файл) в объект типа
IDataSet
данные, которые, как мы ожидаем, должны быть в базе данных.
Вот как выглядит файл SalaryTestDataSet_Expected.xml:
<dataset>
<EMPLOYEE ID="1" SALARY="883.5"/>
<EMPLOYEE ID="2" SALARY="1538.55"/>
<EMPLOYEE ID="3" SALARY="369.72"/>
</dataset>
Обратите внимание, что здесь мы задаем только те данные, которые мы хотим проверить.
В строке 7 мы получаем таблицу
EMPLOYEE
из проверочного источника данных, хотя эта таблица единственная в этом файле, но мы можем определять и другие таблицы.
В строке 8 мы фильтруем столбцы таблицы, полученной из базы данных (стока 5), и выбираем только те столбцы, которые мы хотим проверить, т.е. те, которые мы определили в проверочном источнике данных, в данном случае в файле /data/SalaryTestDataSet_Expected.xml.
В строке 9 мы проверяем ожидаемые данные против данных, полученных из базы данных.
Если данные совпадают – тест успешно пройден, иначе – терпит неудачу.
Преимущества DBUnit
Чтобы протестировать метод updateAllSalaries, к примеру, с помощью JUnit необходимо писать код, который перед вызовом теста создаст и сохранит новую запись о сотруднике в таблице EMPLOYEE, а также заполнит друге таблицы необходимыми данными для анализа и изменения зарплаты. После того как тестируемый метод закончит работу, необходимо написать код, который проверит правильность нового значения в поле SALARY таблицы EMPLOYEE для каждого сотрудника. Данный подход становится все более не приемлемым с ростом сложности тестируемой функциональности, т.к. вам нужно писать все больше и больше кода, чтобы проверить все ветви алгоритма, выполняемого тестируемым методом.
Дополнительные свойства
– Помимо XML файлов DBUnit также поддерживает следующие источники данных: CVS файлы, Excel таблицы, базы данных.
– По умолчанию DBUnit удалит все данные из таблиц, в которые буду загружены
данные для теста, а после окончания теста измененные данные останутся в таблицах. Данное поведение можно изменить, переопределив методы:
@Override
protected DatabaseOperation getSetUpOperation() throws Exception {
return DatabaseOperation.CLEAN_INSERT;
}
@Override
protected DatabaseOperation getTearDownOperation() throws Exception {
return DatabaseOperation.NONE;
}
и используя другие значения класса DatabaseOperation: UPDATE, INSERT, REFRESH, DELETE, DELETE_ALL, TRUNCATE_TABLE.
Также DBUnit обладает возможностью генерирования XML-данных на основании данных уже находящихся в базе данных, которые после можно будет загружать в базу данных для использования в тестах.
Ресурсы
Более подробную информацию вы найдете на сайте www.dbunit.org.
Статья о том, как трансформировать XML файлы Hibernate в плоские XML файлы DBUnit.
Понравилась статья? Подпишись на обновления по RSS/E-mail


(12 голосов, средний: 4.5 из 5)
Cool,
А что же с недостатками DbUnit-а ?? )
Лучше всего использовать его с JndiDataSource-ом, т.к. тогда можно будет обернуть тестовые методы в Jta транзакции
и забыть о том что после теста нужно что-то удалять, а просто откатывать транзакцию (разумеется тестовый код также должен использовать Jta).
smith_and_wesson, у DBUnit конечно же есть недостатки.
К примеру, могут возникнуть сложности если использовать одну базу данных для всех разработчиков, которая уже содержит данные, которые нельзя трогать.
Вы правильно заметили о транзакциях, т.к. DBUnit по-умолчанию не работает с транзакциями (т.е. вставка данных в базу данных и последующее их удаление - это 2 различных транзакции),
поэтому если, к примеру, упадет JVM, данные останутся в базе данных.
Конечно все можно разрешить (прикрутить транзакции), но для тестирования лучше использовать одну базу данных на одного разработчика.
См. Best Practices на сайте DBUnit.
+пиццот !
Согласен.
Однако, это “роскошь”, если проект огромный, обычно есть developer-ская база и база “для тестов”.
Даже если бы была у каждого разработчика своя база, всё равно с ростом проекта росла бы и сложность
поддержания XML-ей DbUnit-а (или с чего там он еще умеет читать). Т.е. на каком-то этапе все заметили бы что тратят
значительное время на то чтобы в правильном порядке расположить блоки тестовых данных в файле для этого фреймворка.
Скажем, есть View кот. поднимается из тонны таблиц, следуя DbUnit-у кому-то нужно разобраться в каком
порядке создать всё эту инфраструктуру чтобы поднять тестовое View. А это - большой головняк.
В этом случае, наверное лучше идти не от БД, а от объектной модели. Тут нужно получить такой DAO уровень чтобы с помощью
него можно было поднять состояние базы. Вот тут наверное лучше всего и стоит применить DbUnit, для 1-2-х таблиц.
Кто-то может прокометнировать аналоги под .NET?
Например, NDbUnit или DbUnit.NET.
Или может есть менее заброшеные проекты?
Не проникся дох писанины да еще на Java.
Отличная статья, коротко и по сути, пасибо
Присоединяюсь к вопросу о реализации под .Net
В “Преимущества DbUnit” приведены лишь проблемы, возникающие при создании сложных тестов - для их инициализации необходимо подготовить большой объем данных. С другой стороны, решение DbUnit как-то не блещет красотой и изяществом - куча инициализационного кода для манипулирования данными на уровне таблиц. Если код, работающий с базой, изолирован он остальных частей системы, возникает проблема портирования тестов при переключении на другие источники данных или использование других реализаций слоя работы с БД.
).
Из личного опыта - все же удобнее использовать хороший тестовый фреймворк (я про TestNG), позволяющий разнести создание сущносей (пользователей, кредитных карт, счетов и пр.) в методах инициализации (@Before) и указать зависимости конкретных тестовых методов от первых путем через параметры аннотации (dependsOnGroups, dependsOnMethods). И все-таки использовать возможности ORM (если она есть).
Касательно примеров есть замечания - было бы круто не загромождать ненужнымм аннотациями, ненужными throws Exception (там где их в помине нет), хардкод параметров теста тоже жесть (слава TestNG!
mind, DBUnit, конечно же, не подходит для всех ситуаций, но в некоторых специальных случаях его использование является наилучшим решением.
От хардкода параметров можно избавиться написав DBUnit-тест другим способом.
Использовали и DBUnit и надстроечку над ним: DDSteps, - весьма полезная тулзовина.