пятница, 27 апреля 2012 г.

DbUnit: пример

Начало: DbUnit: общее описание

Предположим, нам необходимо реализовать тест приложения, сохраняющего данные в БД. Для реализации принципов best practise нам потребуется проинициализировать базу данных (например, очистить её), затем подать на вход приложения какие-то тестовые данные и проверить результат работы приложения, сохранённый в БД. Как это сделать?


1) Создать datasource, например, с помощью Spring в файле "app-ds.xml", подключиться к БД и получить connection для дальнейшей работы с базой:
<bean name="iDataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
    <property name="url" value="jdbc:sqlserver://localhost:1433;username=user;password=pwd;databaseName=dbase"/>
    <property name="maxActive" value="10"/>
    <property name="initialSize" value="5"/>
</bean>
В java коде
import org.apache.commons.dbcp.BasicDataSource;
import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.IDatabaseTester;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

private static IDatabaseTester dbTester = null;
private static IDatabaseConnection dbConnection = null;

private static void setUpClass () {

    // load context
    ApplicationContext ctx = new ClassPathXmlApplicationContext("app-ds.xml");
   
    // configure dbunit
    dbTester = new DataSourceDatabaseTester(ctx.getBean("iDataSource", BasicDataSource.class));
   
    // set start operation (here - cleaning)
    dbTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);  

    // set finish operation (here - nothing)
    dbTester.setTearDownOperation(DatabaseOperation.NONE);          

    // try to connect and get a connection
    dbConnection = dbTester.getConnection();
}
DbUnit реализует несколько вариантов DatabaseOperation, кроме уже перечисленных это DELETE, UPDATE, INSERT, REFRESH.

Параметры initialSize и maxActive определяют начальный и максимальный размер пула создаваемых соединений. В данном случае при создании dbTester будет автоматически сформирован пул из 5 соединений к БД, которые можно получать dbTester.getConnection(). Шестое и последующие соединения будут создаваться непосредственно при необходимости. Если приложение одновременно откроет 10 соединений, то следующий вызов dbTester.getConnection() приведёт к блокировке выполнения программы до тех пор, пока какое-либо из соединений не будет освобождено.

2) Выполнить инициализацию базы данных перед запуском теста. Как уже говорилось чуть раньше, в нашем случае инициализация заключается в очистке. Вопрос в том, какие таблицы очищать? Это можно настроить в отдельном dataset, например, "cleaner.xml":
<dataset>
    <client_message/>
    <send_sms/>
    <result/>
</dataset>
Каждый тэг - название отдельной таблицы. А значит, в одном датасете можно перечислить любые требуемые таблицы.
В java коде инициализация будет выглядеть так:
public void initDababase (IDatabaseTester tester) throws Exception {

    // get resource as stream
    InputStream stream = clazz.getResourceAsStream ("cleaner.xml");  
   
    // parse and build dataset
    IDataSet dataset = new FlatXmlDataSetBuilder().build(source);
   
    // set dataset
    tester.setDataSet(dataset);                                    

    // call setup operation (here - clean up)
    tester.onSetup();
}
В результате три таблицы БД - client_message, send_sms и result - будут очищаться dbunit'ом при вызове метода initDababase.
Для автоматизации вызова этого действия помещаем его в метод с junit-нотацией @Before:
@Before
public void setUpTest () throws Exception {
    initDatabase(...);
}

3) Выполнить тест, т.е. подать на вход тестируемого приложения тестовые данные.

4) Проверить результат работы тестируемого приложения - то, какие данные он сохранил в БД. Для этого настроить новый датасет, например, так:
<dataset>
    <client_message message_identifier="send.1"
                    msisdn="79000000000"
                    priority="0"
                    error_id="[NULL]"
                    status="0" />

    <send_sms sms_index="1"
              body_encoding="1"
              msisdn="79000000000"
              priority="0"
              status="0" />
   
    <result sms_index="1"
              msisdn="79000000000"
              status="0" />
</dataset>
В одном датасете - т.е. в одном файле - настраивается ожидаемое содержимое всех трёх таблиц. Для каждой таблицы заводится отдельный тэг с именем таблицы, а проверяемые поля и их значения указываются как атрибуты этих тэгов.
В java коде
/**
 * Checking of database content.
 * @param connection connection to database;
 * @param source input stream with an expected content;
 * @param tables array of table names.
 * @throws Throwable any trouble during the work including assertion errors.
 */
public static void checkDatabase (IDatabaseConnection connection, InputStream source, String[] tables) throws Throwable {

    IDataSet actualDs = connection.createDataSet();

    //build expected dataset through FlatXmlDataSetBuilder and then decorate it through ReplacementDataSet
    ReplacementDataSet expectedDs = new ReplacementDataSet(new FlatXmlDataSetBuilder().build(source));
    //as the result we can check nulls in easy way
    expectedDs.addReplacementObject("[NULL]", null);

    for (String tableName : tables) {
        ITable expectedTable = getExpectedTable(tableName, expectedDs);
        ITable actualTable = getActualTable(tableName, actualDs, expectedTable.getTableMetaData().getColumns());

        try {
            Assertion.assertEquals(expectedTable, actualTable);
        } catch (AssertionError t) {
            logger.warn("Assertion failed for table " + tableName + "!");
            logger.warn("Expected table content:" + itableToString(expectedTable));
            logger.warn("Actual table content:" + itableToString(actualTable));
            throw t;
        }
    }
}
В результате DbUnit прочитает датасет из файла и сформирует ITable expectedTable, затем прочитает БД и сформирует ITable actualTable, а затем сравнит их и в случае расхождений выдаст ошибку, например, такую:
ERROR [DbUnitAssert] junit.framework.ComparisonFailure: row count (table=client_message) expected:<[1]> but was:<[0]>
WARN  [DbUnitHelper] Assertion failed for table client_message!
WARN  [DbUnitHelper] Expected table content: Row 1: message_identifier="send.1", msisdn="79000000000", priority="0", error_id="null", status="0"
WARN  [DbUnitHelper] Actual table content: Empty table!

Вызов метода checkDatabase удобно поместить под junit-нотацию @After:
@After
public void tearDownTest () throws Exception {
    checkDatabase(...);
}
----------------------------------------------------------------------------------------------------
В результате выполнения перечисленных шагов будет написан тест, в котором инициализация и проверка состояния БД выполняются с помощью DbUnit, а информация для этого содержится в простых и понятных xml файлах.
Далее, при написании следующих тестов весь java код (реализующий коннект к БД, инициализацию, проверку состояния) останется прежним; не изменятся и datasource/datasets. Соответственно, новые тесты потребуют только написание новых датасетов для пункта 4, и всё!

Комментариев нет:

Отправить комментарий