c# - new - transactionscope rollback
Unidad de prueba del uso de TransactionScope (5)
Ahora estoy sentado con el mismo problema y para mí parece haber dos soluciones:
- No resuelvas el problema.
- Cree abstracciones para las clases existentes que siguen el mismo patrón pero son simulables / modestables.
Edición: he creado un proyecto CodePlex para esto ahora: http://legendtransactions.codeplex.com/
Me estoy inclinando hacia la creación de un conjunto de interfaces para trabajar con transacciones y una implementación predeterminada que delega en las implementaciones de Transacciones del Sistema, 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);
}
Esto parece mucho trabajo pero, por otro lado, es reutilizable y hace que todo sea fácilmente comprobable.
Tenga en cuenta que esta no es la definición completa de las interfaces, solo lo suficiente para darle una idea general.
Edit: Acabo de hacer una implementación rápida y sucia como prueba de concepto, creo que esta es la dirección que tomaré, esto es lo que he encontrado hasta ahora. Estoy pensando que tal vez debería crear un proyecto CodePlex para esto para que el problema se pueda resolver 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();
}
}
}
Este es un ejemplo de una clase que depende de ITransactionManager para manejar su 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();
}
}
}
El preámbulo: he diseñado una clase de capa de datos totalmente interconectada y totalmente simulable que espera que la capa de negocios cree un TransactionScope
cuando se deben incluir múltiples llamadas en una sola transacción.
El problema: me gustaría probar por unidad que mi capa empresarial utiliza un objeto TransactionScope
cuando lo espero.
Desafortunadamente, el patrón estándar para usar TransactionScope
es el siguiente:
using(var scope = new TransactionScope())
{
// transactional methods
datalayer.InsertFoo();
datalayer.InsertBar();
scope.Complete();
}
Si bien este es un patrón realmente grande en términos de usabilidad para el programador, las pruebas de que se ha hecho parecen ... imposibles para mí. No puedo detectar que se haya creado una instancia de un objeto transitorio, y mucho menos que se burle de él para determinar que se llamó a un método. Sin embargo, mi objetivo para la cobertura implica que debo hacerlo.
La pregunta: ¿Cómo puedo hacer pruebas de unidades de construcción que aseguren que TransactionScope
se use de manera adecuada de acuerdo con el patrón estándar?
Pensamientos finales: He considerado una solución que sin duda proporcionaría la cobertura que necesito, pero la he rechazado por ser demasiado compleja y no se ajusta al patrón estándar de TransactionScope
. Implica agregar un método CreateTransactionScope
en mi objeto de capa de datos que devuelve una instancia de TransactionScope
. Pero debido a que TransactionScope contiene métodos constructivos y lógicos no virtuales y, por lo tanto, es difícil, si no imposible, burlarse, CreateTransactionScope
devolverá 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 alguna manera mejor?
Después de haber pensado en el mismo problema, llegué a la siguiente solución.
Cambia el patrón a:
using(var scope = GetTransactionScope())
{
// transactional methods
datalayer.InsertFoo();
datalayer.InsertBar();
scope.Complete();
}
protected virtual TransactionScope GetTransactionScope()
{
return new TransactionScope();
}
Cuando luego necesita probar su código, hereda la Clase bajo prueba, extendiendo la función, para que pueda detectar si fue invocado.
public class TestableBLLClass : BLLClass
{
public bool scopeCalled;
protected override TransactionScope GetTransactionScope()
{
this.scopeCalled = true;
return base.GetTransactionScope();
}
}
A continuación, realiza las pruebas relacionadas con TransactionScope en la versión comprobable de su clase.
Encontré una gran manera de probar esto usando Moq y FluentAssertions. Supongamos que su unidad bajo prueba se ve así:
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 se vería así (asumiendo 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.
Ignorar si esta prueba es algo bueno 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 que no sea TransactionScope para lograr esto, pero debería protegerse contra las partes obvias de "no se molestó en tener una transacción".
Otra opción es tratar deliberadamente de crear un nuevo TransactionScope con un nivel de aislamiento incompatible con lo que sea / debería estar en uso y TransactionScopeOption.Required
. Si esto tiene éxito en lugar de lanzar una ArgumentException, no se realizó una transacción. Esto requiere que sepas que un IsolationLevel en particular no se usa (algo como Chaos es una opción potencial)
Ninguna de estas dos opciones es particularmente agradable, esta última es muy frágil y está sujeta a la semántica de TransactionScope que permanece constante. Yo probaría el primero en lugar del último, ya que es algo más robusto (y claro para leer / depurar).
Soy un desarrollador de Java, por lo que no estoy seguro de los detalles de C #, pero me parece que necesita dos pruebas de unidad aquí.
El primero debe ser una prueba de "cielo azul" que tenga éxito. Su prueba de unidad debe garantizar que todos los registros que son ACID aparezcan en la base de datos una vez que se haya confirmado la transacción.
La segunda debe ser la versión "no fiable" que realiza la operación InsertFoo y luego lanza 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 se han confirmado en la base de datos.
Si ambos pasan, diría que su TransactionScope está funcionando como debería.