2009-03-09 6 views
26

der Präambel: ich eine stark angeschlossen und voll Schichtklasse mockable Daten ausgelegt sind, dass die Business-Schicht sollte in einer einzigen Transaktion einbezogen werden ein TransactionScope, wenn mehrere Anrufe erstellen erwartet.Einheit Testen der Verwendung von Transaction

Das Problem: Ich möchte Einheit testen, dass meine Business-Schicht ein TransactionScope Objekt verwendet, wenn ich es erwarte.

Leider ist das Standardmuster TransactionScope für die Verwendung ist wie folgt:

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

Zwar ist dies eine wirklich große Muster in Bezug auf Benutzerfreundlichkeit für den Programmierer ist, die Prüfung, dass es fertig ist scheint ... unpossible mir. Ich kann nicht feststellen, dass ein transientes Objekt instanziiert wurde, geschweige denn es verspotten, um festzustellen, dass eine Methode dafür aufgerufen wurde. Doch mein Ziel für die Berichterstattung bedeutet, dass ich muss.

Die Frage: Wie kann ich über das Erstellen von Unit-Tests gehen, die sicherstellen, TransactionScope wird entsprechend entsprechend dem Standardmuster verwendet?

Final Thoughts: Ich habe eine Lösung in Betracht gezogen, die sicherlich die Abdeckung bieten würde ich brauche, aber haben es als zu komplex abgelehnt und nicht dem Standard TransactionScope Muster entspricht. Es beinhaltet eine CreateTransactionScope Methode für mein Datenschichtobjekt, die eine Instanz von TransactionScope zurückgibt. Aber da TransactionScope Konstruktorlogik und nicht-virtuelle Methoden enthält und daher schwierig, wenn nicht unmöglich zu mocksen ist, würde CreateTransactionScope eine Instanz DataLayerTransactionScope zurückgeben, die eine anschauliche Fassade in TransactionScope wäre.

Während dies den Job erledigen könnte, ist es komplex und ich würde lieber das Standardmuster verwenden. Gibt es einen besseren Weg?

+0

Vielen Dank für diese wertvolle Antwort! ich habe eine que. Kann ich das mit ES DB (NoSQL) verwenden? –

Antwort

28

Ich bin jetzt nur noch mit dem gleichen Problem zu sitzen und mir scheint es zwei Lösungen zu sein:

  1. Sie das Problem nicht lösen.
  2. Erstellen Sie Abstraktionen für die vorhandenen Klassen, die dem gleichen Muster folgen, aber mockbar/stubable sind.

Edit: ich ein CodePlex-Projekt für das jetzt erstellt haben: Ich bin Neigung in Richtung mit Transaktionen für die Arbeit eine Reihe von Schnittstellen zu schaffen und eine Standardimplementierung http://legendtransactions.codeplex.com/

dass die Delegierten System.Transaction-Implementierungen, so etwas wie:

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); 

} 

Dies scheint eine Menge Arbeit, aber auf der anderen Seite ist es wieder verwendbar und es macht es sehr leicht prüfbar.

Beachten Sie, dass dies nicht die vollständige Definition der Schnittstellen gerade genug ist, um Ihnen das große Bild zu geben.

Edit: Ich habe gerade ein paar schnelle und schmutzige Implementierung als Proof of Concept, ich denke, das ist die Richtung, nehme ich, hier ist was ich mit so weit habe kommen. Ich denke, dass ich vielleicht ein CodePlex-Projekt dafür erstellen sollte, damit das Problem ein für allemal gelöst werden kann. Dies ist nicht das erste Mal, dass ich darauf gestoßen bin.

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

Hier ist ein Beispiel einer Klasse, die auf dem ITransactionManager hängt es zu handhaben Transaktions Arbeit:

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

Ich hatte Angst davor. Sagen Sie mir, wie behandeln Sie die Instanziierung von TransactionScope? Erzwingen Sie die Instanziierung durch irgendeine Form von Fabrik und verspotten Sie die Fabrik, um das verspottete TransactionScope auszugeben? – Randolpho

+0

Der ITransactionManager ist in diesem Fall ab Werk die CreateScope-Methode. Dies ist ein Service, den ich Klassen je nach Transaktionsverarbeitung injizieren würde, alternativ könnte ein Service Locator verwendet werden. –

+0

Hey, ich bin gerade über deine Bearbeitung gestolpert. LegendTransactions sieht gut aus! – Randolpho

3

Ich bin ein Java-Entwickler, also bin ich unsicher über die C# -Details, aber es scheint mir, dass Sie hier zwei Komponententests benötigen.

Der erste sollte ein "blauer Himmel" -Test sein, der erfolgreich ist. Ihr Komponententest sollte sicherstellen, dass alle Datensätze, die ACID sind, in der Datenbank angezeigt werden, nachdem die Transaktion festgeschrieben wurde.

Die zweite sollte "wonky" Version sein, die die InsertFoo-Operation ausführt und dann eine Ausnahme auslöst, bevor Sie die InsertBar versuchen. Ein erfolgreicher Test zeigt, dass die Ausnahme ausgelöst wurde und dass weder die Foo- noch die Bar-Objekte an die Datenbank übergeben wurden.

Wenn diese beiden bestehen, würde ich sagen, dass Ihr TransactionScope funktioniert, wie es sollte.

+0

Leider möchte ich diesen Teil des Systems nicht integrieren. Während meiner Komponententests wird die Datenschicht verspottet und es werden keine Verbindungen zur Datenbank hergestellt. Ich kann testen, dass die verschiedenen Methodenaufrufe, die ich erwarte, auftreten; Ich mache mir Sorgen, ob das TransactionScope erstellt wird. – Randolpho

+0

Im Wesentlichen möchte ich wiederholbare Komponententests, die ich häufig und schnell kann; zu testen, dass eine Zeile eingefügt wurde oder nicht, wird es nicht für mich schneiden. Aber danke für die Antwort! :) – Randolpho

+0

Ich denke, es ist wiederholbar und schnell genug - nur meine Meinung. Es scheint sehr deterministisch zu sein - der eine wird erfolgreich sein, der andere nicht. Dies ist nicht wirklich ein Persistenztest, sondern ein Service-Layer-Test. Die Datenbank würde NICHT verspottet werden, aber die Service-Stufe wäre, wenn ich dies tun würde. – duffymo

5

ignorierend, ob dieser Test eine gute Sache ist oder nicht ....

Very dirty Hack ist, dass Transaction.Current zu überprüfen, ist nicht null.

Dies ist kein 100% -Test, da jemand etwas anderes als TransactionScope verwenden könnte, um dies zu erreichen, aber es sollte vor den offensichtlichen "keine Mühe, eine Transaktion zu haben" Teile schützen.

Eine andere Option besteht darin, absichtlich zu versuchen, ein neues TransactionScope mit inkompatibler Isolationsstufe zu dem zu erstellen, was in Verwendung sein sollte und TransactionScopeOption.Required.Wenn dies gelingt, anstatt eine ArgumentException auszulösen, gab es keine Transaktion. Dies erfordert, dass Sie wissen, dass eine bestimmte IsolationLevel nicht verwendet wird (etwas wie Chaos ist eine mögliche Wahl)

Keine dieser beiden Optionen ist besonders angenehm, letztere ist sehr anfällig und unterliegt der Semantik von TransactionScope konstant bleiben. Ich würde das erstere anstelle des letzteren testen, da es etwas robuster ist (und klar zu lesen/zu debuggen).

+0

Ich bin vielleicht dabei, den Null-Check zu machen; Ich hoffe, dass es andere Möglichkeiten gibt. In Bezug darauf, ob der Test eine gute Sache ist oder nicht ... Könnten Sie Ihre Meinung dazu abgeben? Denken Sie, ich sollte nicht feststellen, ob eine Transaktion erstellt wurde? Oder ist es der Wunsch zu verspotten, mit dem du nicht einverstanden bist? – Randolpho

+1

ist es nicht das Spott. Es ist das Beharren, dass die Verbraucher dieser API Transaktionen nutzen. Für eine einzelne Abfrage ist keine explizite Transaktion erforderlich. es kann auch Probleme für Leute verursachen, wenn es den DTM verursacht (ein Schmerz, den ich erlitten habe) – ShuggyCoUk

+1

Ich würde auch vorsichtig sein, ob dieser Test ein falsches Gefühl der Sicherheit ist. Dinge, die Transaktionen für die Richtigkeit erfordern, ist schwierig. Einfach eine Transaktion zu haben, kann nicht genug sein ... – ShuggyCoUk

0

Nachdem ich mich durch das gleiche Problem gedacht zu haben, kam ich zu folgenden Lösung.

Ändern Sie das Muster auf:

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

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

Wenn Sie dann Ihren Code testen müssen, erben Sie die Klasse im Test, die Funktion erstreckt, so dass Sie erkennen können, wenn sie aufgerufen wurde.

public class TestableBLLClass : BLLClass 
    { 
     public bool scopeCalled; 

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

Anschließend führen Sie die Tests in Bezug auf TransactionScope in der testbaren Version Ihrer Klasse durch.

1

Ich fand eine gute Möglichkeit, dies mit Moq und FluentAssertions zu testen.Angenommen, Ihr Gerät unter Test wie folgt aussieht:

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

Ihr Test würde wie folgt aussehen (vorausgesetzt, 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(); 
    } 
} 

Das ist für meine Zwecke funktioniert gut.