Модульное тестирование использования TransactionScope - PullRequest
27 голосов
/ 09 марта 2009

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

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

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

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

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

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

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

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

Ответы [ 5 ]

28 голосов
/ 10 марта 2009

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

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

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's 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();
        }
    }
}
5 голосов
/ 09 марта 2009

Не обращая внимания, хорош ли этот тест или нет ....

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

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

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

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

3 голосов
/ 14 октября 2014

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

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

3 голосов
/ 09 марта 2009

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

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

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

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

0 голосов
/ 20 февраля 2013

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

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

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

...