Модульное тестирование использования TransactionScope

Преамбула: я разработал сильно интерфейсный и полностью фиктивный класс уровня данных, который ожидает, что бизнес-уровень создаст TransactionScope, когда несколько вызовов должны быть включены в одну транзакцию.

Проблема: я хочу проверить, использует ли мой бизнес-уровень объект TransactionScope, когда я этого ожидаю.

К сожалению, стандартный шаблон использования TransactionScope выглядит следующим образом:

using(var scope = new TransactionScope())
{
    // transactional methods
    datalayer.InsertFoo();
    datalayer.InsertBar();
    scope.Complete();
}

Хотя это действительно отличный шаблон с точки зрения удобства использования для программиста, тестирование того, что это сделано, кажется мне... невозможным. Я не могу обнаружить, что временный объект был создан, не говоря уже о том, чтобы смоделировать его, чтобы определить, что для него был вызван метод. Тем не менее, моя цель для освещения подразумевает, что я должен.

Вопрос. Как мне создать модульные тесты, которые гарантируют, что TransactionScope используется надлежащим образом в соответствии со стандартным шаблоном?

Заключительные мысли. Я рассматривал решение, которое, безусловно, обеспечило бы необходимое мне покрытие, но отклонил его как слишком сложное и не соответствующее стандартному шаблону TransactionScope. Это включает в себя добавление метода CreateTransactionScope к моему объекту уровня данных, который возвращает экземпляр TransactionScope. Но поскольку TransactionScope содержит логику конструктора и невиртуальные методы и, следовательно, его трудно, если вообще возможно, имитировать, CreateTransactionScope вернет экземпляр DataLayerTransactionScope, который будет фиктивным фасадом в TransactionScope.

Хотя это может выполнять работу, это сложно, и я бы предпочел использовать стандартный шаблон. Есть ли способ лучше?


person Randolpho    schedule 09.03.2009    source источник
comment
Большое спасибо за этот ценный ответ! у меня есть одна кью. Могу ли я использовать это с ES DB (NoSQL)?   -  person Unknown_Coder    schedule 05.10.2017


Ответы (5)


Я сейчас сижу с той же проблемой, и мне кажется, что есть два решения:

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

Изменить: я создал для этого проект CodePlex: http://legendtransactions.codeplex.com/

Я склоняюсь к созданию набора интерфейсов для работы с транзакциями и реализации по умолчанию, которая делегирует реализации System.Transaction, например:

public interface ITransactionManager
{
    ITransaction CurrentTransaction { get; }
    ITransactionScope CreateScope(TransactionScopeOption options);
}

public interface ITransactionScope : IDisposable
{
    void Complete();  
}

public interface ITransaction
{
    void EnlistVolatile(IEnlistmentNotification enlistmentNotification);
}

public interface IEnlistment
{ 
    void Done();
}

public interface IPreparingEnlistment
{
    void Prepared();
}

public interface IEnlistable // The same as IEnlistmentNotification but it has
                             // to be redefined since the Enlistment-class
                             // has no public constructor so it's not mockable.
{
    void Commit(IEnlistment enlistment);
    void Rollback(IEnlistment enlistment);
    void Prepare(IPreparingEnlistment enlistment);
    void InDoubt(IEnlistment enlistment);

}

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

Обратите внимание, что это не полное определение интерфейсов, достаточное для того, чтобы дать вам общую картину.

Редактировать: я только что сделал несколько быстрых и грязных реализаций в качестве доказательства концепции, я думаю, что это то направление, в котором я буду двигаться, вот что я придумал на данный момент. Я думаю, что, возможно, мне следует создать для этого проект CodePlex, чтобы проблема могла быть решена раз и навсегда. Я не первый раз с этим сталкиваюсь.

public interface ITransactionManager
{
    ITransaction CurrentTransaction { get; }
    ITransactionScope CreateScope(TransactionScopeOption options);
}

public class TransactionManager : ITransactionManager
{
    public ITransaction CurrentTransaction
    {
        get { return new DefaultTransaction(Transaction.Current); }
    }

    public ITransactionScope CreateScope(TransactionScopeOption options)
    {
        return new DefaultTransactionScope(new TransactionScope());
    }
}

public interface ITransactionScope : IDisposable
{
    void Complete();  
}

public class DefaultTransactionScope : ITransactionScope
{
    private TransactionScope scope;

    public DefaultTransactionScope(TransactionScope scope)
    {
        this.scope = scope;
    }

    public void Complete()
    {
        this.scope.Complete();
    }

    public void Dispose()
    {
        this.scope.Dispose();
    }
}

public interface ITransaction
{
    void EnlistVolatile(Enlistable enlistmentNotification, EnlistmentOptions enlistmentOptions);
}

public class DefaultTransaction : ITransaction
{
    private Transaction transaction;

    public DefaultTransaction(Transaction transaction)
    {
        this.transaction = transaction;
    }

    public void EnlistVolatile(Enlistable enlistmentNotification, EnlistmentOptions enlistmentOptions)
    {
        this.transaction.EnlistVolatile(enlistmentNotification, enlistmentOptions);
    }
}


public interface IEnlistment
{ 
    void Done();
}

public interface IPreparingEnlistment
{
    void Prepared();
}

public abstract class Enlistable : IEnlistmentNotification
{
    public abstract void Commit(IEnlistment enlistment);
    public abstract void Rollback(IEnlistment enlistment);
    public abstract void Prepare(IPreparingEnlistment enlistment);
    public abstract void InDoubt(IEnlistment enlistment);

    void IEnlistmentNotification.Commit(Enlistment enlistment)
    {
        this.Commit(new DefaultEnlistment(enlistment));
    }

    void IEnlistmentNotification.InDoubt(Enlistment enlistment)
    {
        this.InDoubt(new DefaultEnlistment(enlistment));
    }

    void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
    {
        this.Prepare(new DefaultPreparingEnlistment(preparingEnlistment));
    }

    void IEnlistmentNotification.Rollback(Enlistment enlistment)
    {
        this.Rollback(new DefaultEnlistment(enlistment));
    }

    private class DefaultEnlistment : IEnlistment
    {
        private Enlistment enlistment;

        public DefaultEnlistment(Enlistment enlistment)
        {
            this.enlistment = enlistment;
        }

        public void Done()
        {
            this.enlistment.Done();
        }
    }

    private class DefaultPreparingEnlistment : DefaultEnlistment, IPreparingEnlistment
    {
        private PreparingEnlistment enlistment;

        public DefaultPreparingEnlistment(PreparingEnlistment enlistment) : base(enlistment)
        {
            this.enlistment = enlistment;    
        }

        public void Prepared()
        {
            this.enlistment.Prepared();
        }
    }
}

Вот пример класса, который зависит от ITransactionManager для обработки своей транзакционной работы:

public class Foo
{
    private ITransactionManager transactionManager;

    public Foo(ITransactionManager transactionManager)
    {
        this.transactionManager = transactionManager;
    }

    public void DoSomethingTransactional()
    {
        var command = new TransactionalCommand();

        using (var scope = this.transactionManager.CreateScope(TransactionScopeOption.Required))
        {
            this.transactionManager.CurrentTransaction.EnlistVolatile(command, EnlistmentOptions.None);

            command.Execute();
            scope.Complete();
        }
    }

    private class TransactionalCommand : Enlistable
    {
        public void Execute()
        { 
            // Do some work here...
        }

        public override void Commit(IEnlistment enlistment)
        {
            enlistment.Done();
        }

        public override void Rollback(IEnlistment enlistment)
        {
            // Do rollback work...
            enlistment.Done();
        }

        public override void Prepare(IPreparingEnlistment enlistment)
        {
            enlistment.Prepared();
        }

        public override void InDoubt(IEnlistment enlistment)
        {
            enlistment.Done();
        }
    }
}
person Patrik Hägne    schedule 09.03.2009
comment
Я боялся этого. Скажите, как вы справляетесь с перехватом экземпляра TransactionScope? Принуждаете ли вы создание экземпляра через какую-либо форму фабрики и издеваетесь над фабрикой, чтобы выдать фиктивный TransactionScope? - person Randolpho; 10.03.2009
comment
ITransactionManager в данном случае и есть эта фабрика, у него есть метод CreateScope. Это сервис, который я бы внедрил в классы, зависящие от обработки транзакций, в качестве альтернативы можно использовать локатор сервисов. - person Patrik Hägne; 10.03.2009
comment
Эй, я только что наткнулся на вашу правку. LegendTransactions выглядит великолепно! - person Randolpho; 11.11.2009
comment
Я думаю, что вы отлично справляетесь... Я обязательно проверю вашу работу над codeplex... странно, что люди из MS не включили это в свою структуру... - person Jalal El-Shaer; 20.01.2010
comment
Пожалуйста, какой-нибудь хороший пример кода с использованием IEnlistmentNotification? Спасибо. - person Kiquenet; 20.07.2010
comment
Пакет NuGet был бы хорош. Я сам создал фабричную абстракцию для создания того, что я называю IUnitOfWork интерфейсами, но это тоже здорово, поскольку позволяет осуществлять фактическое зачисление с использованием библиотеки System.Transactions. Я мог бы регистрировать все изменения или что-то в этом роде в своих модульных тестах. - person Issa Fram; 16.02.2015
comment
Большое спасибо за этот ценный ответ! у меня есть одна кью. Могу ли я использовать это с ES DB? - person Unknown_Coder; 05.10.2017

Игнорирование того, хорош этот тест или нет....

Очень грязный хак - проверить, что Transaction.Current не равен нулю.

Это не 100% тест, поскольку кто-то может использовать что-то другое, кроме TransactionScope, для достижения этого, но он должен защищать от очевидных частей «не удосужился провести транзакцию».

Другой вариант — намеренно попытаться создать новый TransactionScope с несовместимым уровнем изоляции с тем, что будет/должно использоваться, и TransactionScopeOption.Required. Если это удается, а не вызывает исключение ArgumentException, транзакции не было. Это требует, чтобы вы знали, что конкретный уровень изоляции не используется (что-то вроде Хаоса — потенциальный выбор).

Ни один из этих двух вариантов не особенно удобен, последний очень хрупок и зависит от семантики TransactionScope, остающейся неизменной. Я бы протестировал первый, а не второй, поскольку он несколько более надежен (и понятен для чтения/отладки).

person ShuggyCoUk    schedule 09.03.2009
comment
Я могу застрять, выполняя нулевую проверку; Я все же надеюсь, что есть и другие варианты. Что касается того, хорош тест или нет... не могли бы вы все-таки высказать свое мнение? Как вы думаете, мне не стоит утруждать себя определением того, была ли создана транзакция? Или это желание поиздеваться, с чем вы не согласны? - person Randolpho; 09.03.2009
comment
это не издевательство. это настойчивое требование, чтобы потребители этого API использовали транзакции. Для одного запроса не требуется явная транзакция. это также может вызвать проблемы у людей, если это приведет к срабатыванию DTM (боль, которую я испытал) - person ShuggyCoUk; 10.03.2009
comment
Я бы также с осторожностью относился к тому, не является ли этот тест ложным чувством безопасности. иметь дело с вещами, которые требуют транзакций для корректности, сложно. Просто совершить транзакцию может быть недостаточно... - person ShuggyCoUk; 10.03.2009

Я нашел отличный способ проверить это с помощью Moq и FluentAssertions. Предположим, ваш тестируемый модуль выглядит так:

public class Foo
{
    private readonly IDataLayer dataLayer;

    public Foo(IDataLayer dataLayer)
    {
        this.dataLayer = dataLayer;
    }

    public void MethodToTest()
    {
        using (var transaction = new TransactionScope())
        {
            this.dataLayer.Foo();
            this.dataLayer.Bar();
            transaction.Complete();
        }
    }
}

Ваш тест будет выглядеть так (при условии MS Test):

[TestClass]
public class WhenMethodToTestIsCalled()
{
    [TestMethod]
    public void ThenEverythingIsExecutedInATransaction()
    {
        var transactionCommitted = false;
        var fooTransaction = (Transaction)null;
        var barTransaction = (Transaction)null;

        var dataLayerMock = new Mock<IDataLayer>();

        dataLayerMock.Setup(dataLayer => dataLayer.Foo())
                     .Callback(() =>
                               {
                                   fooTransaction = Transaction.Current;
                                   fooTransaction.TransactionCompleted +=
                                       (sender, args) =>
                                       transactionCommitted = args.Transaction.TransactionInformation.Status == TransactionStatus.Committed;
                               });

        dataLayerMock.Setup(dataLayer => dataLayer.Bar())
                     .Callback(() => barTransaction = Transaction.Current);

        var unitUnderTest = new Foo(dataLayerMock.Object);

        unitUnderTest.MethodToTest();

        // A transaction was used for Foo()
        fooTransaction.Should().NotBeNull();

        // The same transaction was used for Bar()
        barTransaction.Should().BeSameAs(fooTransaction);

        // The transaction was committed
        transactionCommitted.Should().BeTrue();
    }
}

Это прекрасно работает для моих целей.

person Mathias Becher    schedule 14.10.2014

Я разработчик Java, поэтому я не уверен в деталях C#, но мне кажется, что здесь вам нужны два модульных теста.

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

Вторая версия должна быть «шаткой», которая выполняет операцию InsertFoo, а затем выдает исключение перед попыткой InsertBar. Успешный тест покажет, что было создано исключение и что ни объекты Foo, ни Bar не были зафиксированы в базе данных.

Если оба они пройдены, я бы сказал, что ваш TransactionScope работает как надо.

person duffymo    schedule 09.03.2009
comment
К сожалению, я не планирую проводить интеграционное тестирование этой части системы; во время моих модульных тестов уровень данных будет имитироваться, и никаких подключений к базе данных не будет. Я могу проверить, происходят ли ожидаемые вызовы различных методов; что меня беспокоит, так это создание TransactionScope. - person Randolpho; 09.03.2009
comment
По сути, мне нужны повторяемые модульные тесты, которые я могу выполнять часто и быстро; проверка того, была ли строка вставлена ​​или нет, не поможет мне. Но спасибо за ответ! :) - person Randolpho; 09.03.2009
comment
Я думаю, что это воспроизводимо и достаточно быстро - только мое мнение. Это кажется очень детерминированным - у одного получится, у другого нет. На самом деле это не тест на устойчивость, а тест уровня обслуживания. База данных НЕ будет издеваться, но уровень обслуживания был бы, если бы я это делал. - person duffymo; 09.03.2009
comment
Я понимаю, откуда вы исходите, но я не думаю, что это соответствует тому, что я ищу в своем вопросе. У меня уже есть интеграционные тесты, которые удовлетворяют мой уровень данных. Теперь я хочу провести модульное тестирование использования уровня данных моего бизнес-уровня. Я не хочу делать это с интеграционным тестом - person Randolpho; 09.03.2009
comment
Опять же, я понимаю, что вы говорите, и я мог бы сделать это в других ситуациях, но, к сожалению, (по разным бюрократическим причинам) я не могу провести этот тест с реальной базой данных. Я должен издеваться над без подключения. - person Randolpho; 09.03.2009

Подумав сам над тем же вопросом, я пришел к следующему решению.

Измените шаблон на:

using(var scope = GetTransactionScope())
{
    // transactional methods
    datalayer.InsertFoo();
    datalayer.InsertBar();
    scope.Complete();
}

protected virtual TransactionScope GetTransactionScope()
{
    return new TransactionScope();
}

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

public class TestableBLLClass : BLLClass
    {
        public bool scopeCalled;

        protected override TransactionScope GetTransactionScope()
        {
            this.scopeCalled = true;
            return base.GetTransactionScope();
        }
    }

Затем вы выполняете тесты, относящиеся к TransactionScope, на тестируемой версии вашего класса.

person Grubsnik    schedule 20.02.2013
comment
Образец модульного теста? - person Kiquenet; 26.11.2020
comment
У меня нет образца, но я бы просто вызвал функцию как обычно, а затем проверил, что логическое значение scopeCalled истинно - person Grubsnik; 27.11.2020