2009-03-09 3 views
26

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

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

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

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

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

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

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

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

+0

Большое спасибо за этот ценный ответ! У меня есть одна очередь. Могу ли я использовать это с ES DB (NoSQL)? –

ответ

28

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

  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(); 
     } 
    } 
} 
+0

Я этого боялся. Скажите, как вы справляетесь с созданием TransactionScope? Вы вынуждаете создание экземпляра через какую-либо фабрику и издеваетесь над фабрикой, чтобы испустить издеваемую TransactionScope? – Randolpho

+0

В этом случае ITransactionManager - это фабрика, у нее есть метод CreateScope. Это сервис, который я бы вводил в классы, зависящие от обработки транзакций, в качестве альтернативы можно было бы использовать локатор сервисов. –

+0

Эй, я просто наткнулся на твою правку. LegendTransactions отлично смотрится! – Randolpho

3

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

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

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

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

+0

К сожалению, я не планирую интегрировать тест этой части системы; во время моих модульных тестов слой данных будет издеваться и никаких подключений к базе данных не произойдет. Я могу проверить, что вызовы различных методов, которые я ожидаю, происходят; я беспокоюсь о том, создан ли TransactionScope. – Randolpho

+0

По существу, я хочу повторять модульные тесты, которые я могу часто и быстро; тестирование того, что строка была вставлена ​​или нет, не собирается сокращать ее для меня. Но спасибо за ответ! :) – Randolpho

+0

Я думаю, что это повторяемо и достаточно быстро - просто мое мнение. Это кажется очень детерминированным - один преуспеет, другой - нет. Это не тест на персистенцию, а тест уровня сервиса. База данных НЕ будет издеваться, но уровень обслуживания будет, если я это сделаю. – duffymo

5

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

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

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

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

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

+0

Я могу застрять, выполняя нулевую проверку; Я надеюсь, что есть и другие варианты. Относительно того, хорошо ли тест или нет ... Могли бы вы высказать свое мнение в конце концов? Как вы думаете, я не должен беспокоиться о том, была ли создана транзакция? Или это желание издеваться над тем, с чем вы не согласны? – Randolpho

+1

это не насмешка. это настояние, что потребители этого апи используют транзакции. Для одного запроса не требуется явная транзакция. это также может вызвать проблемы для людей, если это заставляет DTM удариться (боль, которую я перенесла) – ShuggyCoUk

+1

Я также опасаюсь, является ли этот тест ложным чувством безопасности. иметь дело с вещами, которые требуют транзакций для правильности, трудно. Просто иметь транзакцию может быть недостаточно ... – ShuggyCoUk

0

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

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

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

1

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

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

Смежные вопросы