Подтвердить что ты не робот

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Edit: Я создал для этого проект 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 not mockable.
{
    void Commit(IEnlistment enlistment);
    void Rollback(IEnlistment enlistment);
    void Prepare(IPreparingEnlistment enlistment);
    void InDoubt(IEnlistment enlistment);

}

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

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

Edit: Я просто сделал какую-то быструю и грязную реализацию как доказательство концепции, я думаю, что это направление, которое я возьму, вот что я до сих пор придумал. Я думаю, что, возможно, я должен создать проект 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();
        }
    }
}

Ответ 2

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

Очень грязный взломать, чтобы проверить, что Transaction.Current не является нулевым.

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

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

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

Ответ 3

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

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

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

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

Ответ 4

Я нашел отличный способ проверить это с помощью 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();
    }
}

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

Ответ 5

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

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

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, в проверяемой версии вашего класса.