Каков наилучший способ вернуть базу данных в известное состояние при тестировании операций базы данных? - PullRequest
4 голосов
/ 05 сентября 2011

Я пишу тесты с JUnit для некоторых методов, работающих с тестовой базой данных.

Мне нужно сбрасывать базу данных в исходное состояние после каждого @Test. Мне интересно, как лучше это сделать.

Есть ли какой-нибудь метод в EntityManager? Или я должен просто удалить все вручную или с помощью оператора SQL? Было бы лучше просто удалить и воссоздать всю базу данных?

Ответы [ 4 ]

3 голосов
/ 05 сентября 2011

Самый простой способ - просто откатить все изменения после каждого теста. Для этого требуется транзакционная СУБД и пользовательский тестовый прогон или аналогичный, который оборачивает каждый тест в свою собственную транзакцию. Spring's AbstractTransactionalJUnit4SpringContextTests делает именно это.

3 голосов
/ 05 сентября 2011

Одна из техник, которую я использовал в прошлом, заключается в воссоздании базы данных с нуля, просто копируя базу данных из стандартной «тестовой базы данных» и используя ее в тестах.

Эта методика работает, если:

  1. Ваша схема не сильно меняется (в противном случае сложно оставаться в курсе)
  2. Вы используете что-то вроде hibernate, который достаточно независим от базы данных.

Это имеет следующие преимущества:

  1. Работает с кодом, который управляет своими собственными транзакциями.Мои интеграционные тесты выполняются под юнитом.Например, когда я тестирую пакетный процесс, я вызываю Batch.main () из junit и тестирую вещи до и после.Я не хотел бы изменять обработку транзакций в тестируемом коде.
  2. Это достаточно быстро.Если файлы достаточно малы, скорость не является проблемой.
  3. Это облегчает запуск интеграционных тестов на сервере ci.Файлы базы данных проверяются с помощью кода.Нет необходимости в том, чтобы работала реальная база данных.

И следующие недостатки:

  1. Файлы тестовой базы данных должны поддерживаться вместе с реальной базой данных.Если вы все время добавляете столбцы, это может быть неприятно.
  2. Существует код для управления URL-адресами jdbc, потому что они меняются для каждого теста.

Я используюэто с Oracle в качестве базы данных производства / интеграции и hsqldb в качестве тестовой базы данных.Это работает довольно хорошо.hsqldb - это отдельный файл, поэтому его легко скопировать.

Итак, в @ Before , используя hsqldb, вы копируете файл в такое место, как target / it / database / name_of_test.script.Это обнаруживается в тесте.

В @ После вы удаляете файл (или просто оставляете его, кому какое дело).С hsqldb вам также потребуется выполнить SHUTDOWN, чтобы вы могли удалить файл.

Вы также можете использовать @ Rule , которое расширяется от ExternalResource , который является лучшим способом управления вашими ресурсами.

Еще один совет: если вы используете maven или что-то подобное, вы можете создать базу данных в target.Я использую цель / это.Таким образом, копии баз данных удаляются, когда я делаю и mvn clean.Для своих пакетов я фактически копирую все остальные файлы свойств и т. Д. В этот каталог, так что я не получаю никаких файлов, появляющихся в странных местах.

2 голосов
/ 05 сентября 2011

DBUnit может сбросить вашу базу данных между тестами и даже заполнить ее предопределенными тестовыми данными.

0 голосов
/ 21 ноября 2012

Я отвечаю на это больше для моей собственной справки, но здесь идет. Ответ предполагает наличие базы данных SQL Server для разработчика.

Базовый подход

  1. Используйте DBUnit для хранения XML-файла с известным состоянием. Вы можете извлечь этот файл после того, как вы настроили БД, или вы можете создать его с нуля. Поместите этот файл в свой контроль версий вместе со скриптами, которые вызывают DBUnit для заполнения им БД.

  2. В своих тестах вызывайте вышеупомянутые сценарии, используя @Before.

Ускорение 1

Как только это сработает, настройте подход, чтобы ускорить процесс. Вот подход для БД SQL Server.

Перед DBUnit полностью стереть БД:

EXEC sp_msforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL';
EXEC sp_MSforeachtable 'ALTER TABLE ? DISABLE TRIGGER ALL';
EXEC sp_MSForEachTable 'SET QUOTED_IDENTIFIER ON SET ANSI_NULLS ON DELETE FROM ?';

После DBUnit восстановить ограничения

EXEC sp_MSforeachtable 'ALTER TABLE ? CHECK CONSTRAINT ALL';
EXEC sp_MSforeachtable 'ALTER TABLE ? ENABLE TRIGGER ALL';

Ускорение 2

Использовать функцию восстановления сервера SQL Server. В моих тестах это работает в 25% времени, которое занимает DBUnit. Если (и только если) это является основным фактором продолжительности вашего теста, стоит изучить этот подход.

Следующие классы показывают реализацию, использующую Spring JDBC, JTDS и инъекцию CDI. Это разработано для работы в контейнерах, где контейнер может устанавливать свои собственные соединения с БД, которые необходимо остановить

import java.io.File;
import java.sql.SQLException;

import javax.inject.Inject;
import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;

/**
 * Allows the DB to be reset quickly using SQL restore, at the price of
 * additional complexity. Recommended to vanilla DBUnit unless speed is
 * necessary.
 * 
 * @author aocathain
 * 
 */
@SuppressWarnings({ "PMD.SignatureDeclareThrowsException" })
public abstract class DbResetterSO {

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Deliberately created in the target dir, so that on mvn clean, it is
     * deleted and will be recreated.
     */
    private final File backupFile = new File(
            "target\\test-classes\\db-backup.bak");

    @Inject
    private OtherDbConnections otherDbConnections;

    /**
     * Backs up the database, if a backup doesn't exist.
     * 
     * @param masterDataSource
     *            a datasource with sufficient rights to do RESTORE DATABASE. It
     *            must not be connected to the database being restored, so
     *            should have db master as its default db.
     */
    public void backup(final DataSource masterDataSource) throws Exception {

        final JdbcTemplate masterJdbcTemplate = new JdbcTemplate(
                masterDataSource);

        if (backupFile.exists()) {
            logger.debug("File {} already exists, not backing up", backupFile);
        } else {
            otherDbConnections.start();

            setupDbWithDbUnit();

            otherDbConnections.stop();
            logger.debug("Backing up");
            masterJdbcTemplate.execute("BACKUP DATABASE [" + getDbName()
                    + "] TO DISK ='" + backupFile.getAbsolutePath() + "'");
            logger.debug("Finished backing up");
            otherDbConnections.start();
        }

    }

    /**
     * Restores the database
     * 
     * @param masterDataSource
     *            a datasource with sufficient rights to do RESTORE DATABASE. It
     *            must not be connected to the database being restored, so
     *            should have db master as its default db.
     */
    public void restore(final DataSource masterDataSource) throws SQLException {
        final JdbcTemplate masterJdbcTemplate = new JdbcTemplate(
                masterDataSource);

        if (!backupFile.exists()) {
            throw new IllegalStateException(backupFile.getAbsolutePath()
                    + " must have been created already");
        }
        otherDbConnections.stop();

        logger.debug("Setting to single user");

        masterJdbcTemplate.execute("ALTER DATABASE [" + getDbName()
                + "] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;");

        logger.info("Restoring");

        masterJdbcTemplate.execute("RESTORE DATABASE [" + getDbName()
                + "] FROM DISK ='" + backupFile.getAbsolutePath()
                + "' WITH REPLACE");

        logger.debug("Setting to multi user");

        masterJdbcTemplate.execute("ALTER DATABASE [" + getDbName()
                + "] SET MULTI_USER;");

        otherDbConnections.start();
    }

    /**
     * @return Name of the DB on the SQL server instance
     */
    protected abstract String getDbName();

    /**
     * Sets up the DB to the required known state. Can be slow, since it's only
     * run once, during the initial backup. Can use the DB connections from otherDbConnections.
     */
    protected abstract void setupDbWithDbUnit() throws Exception;
}


import java.sql.SQLException;

/**
 * To SQL RESTORE the db, all other connections to that DB must be stopped. Implementations of this interface must
 * have control of all other connections.
 * 
 * @author aocathain
 * 
 */
public interface OtherDbConnections
{

    /**
     * Restarts all connections
     */
    void start() throws SQLException;

    /**
     * Stops all connections
     */
    void stop() throws SQLException;

}



import java.sql.Connection;
import java.sql.SQLException;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.inject.Produces;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.sql.DataSource;

import net.sourceforge.jtds.jdbcx.JtdsDataSource;

import org.springframework.jdbc.datasource.DelegatingDataSource;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

/**
 * Implements OtherDbConnections for the DbResetter and provides the DataSource during in-container tests.
 * 
 * @author aocathain
 * 
 */
@Singleton
@SuppressWarnings({ "PMD.AvoidUsingVolatile" })
public abstract class ResettableDataSourceProviderSO implements OtherDbConnections
{

    private volatile Connection connection;
    private volatile SingleConnectionDataSource scds;
    private final DelegatingDataSource dgds = new DelegatingDataSource();

    @Produces
    @Named("in-container-ds")
    public DataSource resettableDatasource() throws SQLException
    {
        return dgds;
    }

    @Override
    @PostConstruct
    public void start() throws SQLException
    {
        final JtdsDataSource ds = new JtdsDataSource();

        ds.setServerName("localhost");
        ds.setDatabaseName(dbName());
        connection = ds.getConnection(username(), password());

        scds = new SingleConnectionDataSource(connection, true);
        dgds.setTargetDataSource(scds);

    }

    protected abstract String password();

    protected abstract String username();

    protected abstract String dbName();

    @Override
    @PreDestroy
    public void stop() throws SQLException
    {
        if (null != connection)
        {
            scds.destroy();
            connection.close();
        }

    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...