Групповое тестирование с помощью ручных транзакций и многоуровневых транзакций

Из-за нескольких ограничений я не могу использовать сущность Framework и поэтому должен вручную использовать SQL-соединения, команды и транзакции.

При написании модульных тестов для методов, вызывающих эти операции с данными, я наткнулся на несколько проблем.

Для модульных тестов мне НЕОБХОДИМО выполнять их в транзакции, так как большинство операций меняют данные по своей природе, и поэтому их выполнение вне транзакции проблематично, так как это изменило бы все базовые данные. Таким образом, мне нужно поставить транзакцию вокруг них (без фиксации в конце).

Теперь у меня есть два разных варианта использования этих методов BL. У некоторых есть Сделки внутри себя, в то время как у других нет Сделок вообще. Оба этих варианта вызывают проблемы.

  • Layered Transaction: Здесь я получаю ошибки, которые DTC отменяет распределенную транзакцию из-за тайм-аутов (хотя тайм-аут устанавливается на 15 минут, и он работает всего на 2 минуты).

  • Только 1 транзакция: здесь я получаю сообщение об ошибке состояния транзакции, когда я прихожу к строке "new SQLCommand" в вызываемом методе.

Мой вопрос здесь, что я могу сделать, чтобы исправить это и получить модульное тестирование с ручными нормальными и многослойными транзакциями?

Пример метода тестирования:

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
    connection.Open();
    using (SqlTransaction transaction = connection.BeginTransaction())
    {
        MyBLMethod();
    }
}

Пример транзакции с использованием метода (очень упрощенный)

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
    connection.Open();
    using (SqlTransaction transaction = connection.BeginTransaction())
    {
        SqlCommand command = new SqlCommand();
        command.Connection = connection;
        command.Transaction = transaction;
        command.CommandTimeout = 900;   // Wait 15 minutes before a timeout
        command.CommandText = "INSERT ......";
        command.ExecuteNonQuery();

        // Following commands
        ....

        Transaction.Commit();
    }
}

Пример для не транзакции с использованием метода

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
    connection.Open();

    SqlCommand command = new SqlCommand();
    command.Connection = connection;
    command.CommandTimeout = 900;   // Wait 15 minutes before a timeout
    command.CommandText = "INSERT ......";
    command.ExecuteNonQuery();
}

Ответ 1

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

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

Если вы не против попадания в базу данных, минимальное влияние на изменение вашего кода/расходов будет заключаться в том, чтобы вы могли завершить транзакции и проверить их в базе данных. Вы можете сделать это, сделав снимки базы данных и сбросив базу данных каждый тестовый прогон или имея специальную тестовую базу данных и записывая тесты таким образом, чтобы они могли безопасно удалять базу данных снова и снова, а затем проверять. Например, вы можете вставить запись с инкрементированным идентификатором, обновить запись, а затем проверить, что ее можно прочитать. Возможно, вам придется больше раскручиваться, если есть ошибки, но если вы не изменяете код доступа к данным или структуру базы данных, то часто это не должно быть слишком большой проблемой.

Если вы можете потратить немного денег и хотите на самом деле перевести свои тесты на модульные тесты, чтобы они не попали в базу данных, вам следует рассмотреть возможность поиска в TypeMock. Это очень мощная насмешливая структура, которая может сделать некоторые довольно страшные вещи. Я считаю, что использование API профилирования для перехвата вызовов, а не для использования подхода, используемого такими фреймворками, как Moq. Вот пример использования Typemock для издевательства SQLConnection здесь.

Если у вас нет денег, чтобы потратить/вы можете изменить свой код и не возражаете продолжать полагаться на базу данных, вам нужно взглянуть на какой-то способ поделиться своим соединением с базой данных между вашим тестовым кодом и вашими методами обработки данных. Два подхода, которые spring должны учитывать: либо вводить информацию о соединении в класс, либо делать ее доступной, введя factory, которая дает доступ к информации о соединении (в этом случае вы можете ввести макет factory во время тестирования, который возвращает требуемое соединение).

Если вы перейдете к вышеуказанному подходу, а не прямо вставляете SqlConnection, подумайте о том, чтобы ввести класс оболочки, который также несет ответственность за транзакцию. Что-то вроде:

public class MySqlWrapper : IDisposable {
    public SqlConnection Connection { get; set; }
    public SqlTransaction Transaction { get; set; }

    int _transactionCount = 0;

    public void BeginTransaction() {
        _transactionCount++;
        if (_transactionCount == 1) {
            Transaction = Connection.BeginTransaction();
        }
    }

    public void CommitTransaction() {
        _transactionCount--;
        if (_transactionCount == 0) {
            Transaction.Commit();
            Transaction = null;
        }
        if (_transactionCount < 0) {
            throw new InvalidOperationException("Commit without Begin");
        }
    }

    public void Rollback() {
        _transactionCount = 0;
        Transaction.Rollback();
        Transaction = null;
    }


    public void Dispose() {
        if (null != Transaction) {
            Transaction.Dispose();
            Transaction = null;
        }
        Connection.Dispose();
    }
}

Это предотвратит создание вложенных транзакций +.

Если вы захотите реструктурировать свой код, вы можете захотеть обернуть свой код обработки данных более макетным способом. Так, например, вы можете использовать функциональность доступа к основной базе данных в другой класс. В зависимости от того, что вы делаете, вам нужно расширить его, однако вы можете получить что-то вроде этого:

public interface IMyQuery {
    string GetCommand();
}

public class MyInsert : IMyQuery{
    public string GetCommand() {
        return "INSERT ...";
    }
}

class DBNonQueryRunner {
    public void RunQuery(IMyQuery query) {
        using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString)) {
            connection.Open();
            using (SqlTransaction transaction = connection.BeginTransaction()) {
                SqlCommand command = new SqlCommand();
                command.Connection = connection;
                command.Transaction = transaction;
                command.CommandTimeout = 900;   // Wait 15 minutes before a timeout
                command.CommandText = query.GetCommand();

                command.ExecuteNonQuery();

                transaction.Commit();
            }
        }
    }
}

Это позволяет вам unit test больше вашей логики, например кода генерации команды, не беспокоясь о том, чтобы попасть в базу данных, и вы можете протестировать свой основной код доступа к данным (Runner) по базе данных один раз, а не на каждая команда, которую вы хотите запустить против базы данных. Я все равно буду писать интеграционные тесты для всего кода доступа к данным, но Id только имеет тенденцию запускать их, пока фактически работает над этим разделом кода (чтобы гарантировать, что имена столбцов и т.д. Указаны правильно).