2009-03-09 24 views
26

El preámbulo: He diseñado una clase de capa de datos fuertemente interconectado y totalmente mockable que espera que la capa de negocio para crear un TransactionScope cuando múltiples llamadas deben ser incluidos en una sola transacción.Unidad probando el uso de TransactionScope

El problema es: Me gustaría probar de forma unitaria que mi capa empresarial hace uso de un objeto TransactionScope cuando lo espero.

Por desgracia, el patrón estándar para el uso de TransactionScope es la siguiente:

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

Si bien esto es realmente un gran patrón en términos de facilidad de uso para el programador, las pruebas que se hace parece ... unpossible a mí. No puedo detectar que un objeto transitorio haya sido instanciado, y mucho menos simularlo para determinar que un método fue llamado sobre él. Sin embargo, mi objetivo de cobertura implica que debo hacerlo.

La Pregunta: ¿Cómo puedo ir construyendo unidad de pruebas que aseguran TransactionScope se usa apropiadamente de acuerdo con el patrón estándar?

Consideraciones finales: He pensado en una solución que sin duda proporcionar la cobertura que necesito, pero lo han rechazado como demasiado complejo y que no cumplan el patrón estándar TransactionScope. Implica agregar un método CreateTransactionScope en mi objeto de capa de datos que devuelve una instancia de TransactionScope. Pero como TransactionScope contiene lógica de constructor y métodos no virtuales y, por lo tanto, es difícil, si no imposible, simular, CreateTransactionScope devolvería una instancia de DataLayerTransactionScope que sería una fachada simulada en TransactionScope.

Si bien esto podría hacer el trabajo, es complejo y preferiría usar el patrón estándar. ¿Hay una mejor manera?

+0

Muchas gracias por esta valiosa respuesta! tengo una que. ¿Puedo usar esto con ES DB (NoSQL)? –

Respuesta

28

sólo estoy ahora sentado con el mismo problema y me parece que hay dos soluciones:

  1. no resuelven el problema.
  2. Crea abstracciones para las clases existentes que siguen el mismo patrón pero son modificables/stubable.

Editar: He creado un CodePlex-proyecto para este momento: http://legendtransactions.codeplex.com/

Me estoy inclinando hacia la creación de un conjunto de interfaces para trabajar con transacciones y una aplicación por defecto que los delegados a la System.Transaction-implementaciones, algo así como:

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

} 

Este parece ser un montón de trabajo, pero por otro lado es reutilizable y que hace que sea muy fácil comprobable.

Tenga en cuenta que esta no es la definición completa de las interfaces lo suficiente para darle una idea general.

Editar: que acabo de hacer alguna implementación rápida y sucia como una prueba de concepto, creo que esta es la dirección que va a tomar, esto es lo que he encontrado hasta el momento. Estoy pensando que tal vez debería crear un proyecto CodePlex para que el problema pueda resolverse de una vez por todas. Esta no es la primera vez que me encuentro con esto.

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

He aquí un ejemplo de una clase que depende de la ITransactionManager de manejar que es un trabajo transaccional:

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

Tenía miedo de eso. Dígame, ¿cómo maneja la captura de la creación de instancias de TransactionScope? ¿Impone la creación de instancias a través de algún tipo de fábrica y se burla de la fábrica para emitir el TransactionScope burlado? – Randolpho

+0

El ITransactionManager en este caso es de fábrica, tiene el método CreateScope. Este es un servicio que yo inyectaría a las clases dependiendo del manejo de la transacción, alternativamente podría usarse un localizador de servicio. –

+0

Oye, acabo de tropezar con su edición. LegendTransactions se ve genial! – Randolpho

3

Soy desarrollador de Java, por lo que no estoy seguro de los detalles de C#, pero me parece que aquí se necesitan dos pruebas de unidad.

La primera debe ser una prueba de "cielo azul" que tenga éxito. La prueba de unidad debe garantizar que todos los registros que son ACID aparecen en la base de datos después de que se ha confirmado la transacción.

La segunda debe ser la versión "wonky" que realiza la operación InsertFoo y luego arroja una excepción antes de intentar InsertBar. Una prueba exitosa mostrará que se ha lanzado la excepción y que ni los objetos Foo ni Bar han sido enviados a la base de datos.

Si ambos pasan, diría que su TransactionScope está funcionando como debería.

+0

Lamentablemente, no planeo realizar pruebas de integración en esta parte del sistema; durante mis pruebas unitarias, la capa de datos se burlará y no se producirán conexiones a la base de datos. Puedo probar que ocurren las varias llamadas a métodos que espero; lo que me preocupa es si se crea el TransactionScope. – Randolpho

+0

Básicamente, quiero pruebas repetibles de unidades que pueda con frecuencia y rapidez; probar que una fila se insertó o no no va a ser suficiente para mí. ¡Pero gracias por la respuesta! :) – Randolpho

+0

Creo que es repetible y lo suficientemente rápido, solo mi opinión. Parece muy determinista: uno tendrá éxito, el otro no. Esto no es realmente una prueba de persistencia sino una prueba de capa de servicio. La base de datos NO se burlaría, pero el nivel de servicio sería si estuviera haciendo esto. – duffymo

5

Haciendo caso omiso de si esta prueba es una buena cosa o no ....

truco muy sucio es comprobar que Transaction.Current no es nulo.

Esto no es una prueba del 100%, ya que alguien podría estar usando algo diferente a TransactionScope para lograr esto, pero debería evitar las partes obvias de 'no se molestó en tener una transacción'.

Otra opción es intentar deliberadamente crear un nuevo TransactionScope con nivel de aislamiento incompatible con lo que debería/debería estar en uso y TransactionScopeOption.Required.Si esto tiene éxito en lugar de arrojar una ArgumentException, no hubo una transacción. Esto requiere que usted sepa que un IsolationLevel particular no se utiliza (algo como Chaos es una elección posible)

Ninguna de estas dos opciones es particularmente agradable, esta última es muy frágil y está sujeta a la semántica de TransactionScope que se mantiene constante. Probaría lo primero en lugar de lo último, ya que es algo más robusto (y claro para leer/depurar).

+0

Puedo estar atrapado haciendo la comprobación nula; Espero que haya otras opciones, sin embargo. En cuanto a si la prueba es algo bueno o no ... ¿podría dar su opinión después de todo? ¿Crees que no debería molestarme en determinar si se creó una transacción? ¿O es el deseo de burlarse con el que no está de acuerdo? – Randolpho

+1

no es la burla. es la insistencia de que los consumidores de esta API utilicen las transacciones. Para una consulta única no se requiere una transacción explícita. también puede causar problemas a las personas si hace que el DTM se active (un dolor que he sufrido) – ShuggyCoUk

+1

También me preocuparía si esta prueba es una falsa sensación de seguridad. ocuparse de cosas que requieren transacciones para la corrección es difícil. Simplemente tener una transacción puede no ser suficiente ... – ShuggyCoUk

0

Después de haber reflexionado sobre el mismo tema a mí, llegué a la siguiente solución.

cambiar el patrón a:

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

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

Cuando este caso es necesario para probar el código, que hereda la clase bajo prueba, la ampliación de la función, por lo que puede detectar si se invocó.

public class TestableBLLClass : BLLClass 
    { 
     public bool scopeCalled; 

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

Luego realiza las pruebas relacionadas con TransactionScope en la versión comprobable de su clase.

1

Encontré una excelente manera de probar esto usando Moq y FluentAssertions.Suponga que su unidad bajo prueba es el siguiente:

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

Su prueba sería el siguiente (suponiendo 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(); 
    } 
} 

Esto funciona muy bien para mis propósitos.

Cuestiones relacionadas