Reputation: 11680
I have a pair of tables with a parent/child relationship - incident and incidentDetails. I have a viewmodel that contains information from both of these tables. And I have a business layer method that is passed an instance of the viewmodel that needs to update both tables.
So, in the method, I'm using EF6's new transaction mechanism:
using (var transaction = this.db.Database.BeginTransaction())
{
try
{
// various database stuff
this.db.SaveChanges();
// more database stuff
this.db.SaveChanges();
// yet more database stuff
this.db.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
this.logger.logException(ex, "Exception caught in transaction, rolling back");
throw;
}
}
And so, my problem. How do I test this?
I'm using Microsoft's unit testing framework, with Moq, and I have had no trouble with mocking up DBContexts, and DbSet<>s, but I can't seem to figure out how to get around the transaction stuff.
If I don't attempt to mock the transaction, I get an InvalidOperationException:
"No connecting string named xxx could be found in the application config file."
Which makes perfect sense - there isn't an application config file, and there isn't any database.
But if I try to mock BeginTransaction(), I get initialization errors: NotSupportedException:
"Invalid setup on a non-virtual member: m => m.Database.BeginTransaction".
And that got me chasing into the weeds, looking at decompiles of the .NET methods, trying to identify some class that might derive from a usable interface, or something, where I could somehow inject a mocking object.
I'm not trying to unit-test MS's transactional code - I just want to make sure that the appropriate changes are made to the appropriate records in each of the tables. But as it sits, it looks like this is non-testable, and that any method that uses transactions is non-testable. And that's just a pain.
I've Googled around, and not found anything of use. Has anyone run into this issue? Anyone have ideas on how to proceed?
Upvotes: 26
Views: 26811
Reputation: 21
Since you're using Moq, and your primary goal is to avoid the NotSupportedException on Database.BeginTransaction() so that you can just move on to the test, you can create a mock of DatabaseFacade that returns a transaction that does nothing:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MyNamespace
{
/// <summary>
/// A test DatabaseFacade whose primary purpose is to return a MockTransaction
/// </summary>
public class MockDatabase : DatabaseFacade
{
public MockDatabase(DbContext context) : base(context) { }
public override IDbContextTransaction BeginTransaction()
{
return new MockTransaction();
}
}
/// <summary>
/// A database transaction that does nothing, to be used with a test db that does not
/// natively support transactions, like EntityFrameworkCore.InMemory
/// </summary>
public class MockTransaction : IDbContextTransaction
{
public Guid TransactionId => new();
public void Commit() { }
public Task CommitAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Dispose() { }
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
public void Rollback() { }
public Task RollbackAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
}
Then in your test setup, tell Moq to use the mock DatabaseFacade. Also note that if the testing framework is using a db implementation that doesn't natively support transactions (like Microsoft.EntityFrameworkCore.InMemory), you may get the exception "An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.Transaction.TransactionIgnoredWarning'". You can tell it to ignore the warning in the context builder options:
var dbContextOptions = new DbContextOptionsBuilder<MyDbContext>()
.UseInMemoryDatabase(databaseName: "foo")
.ConfigureWarnings(warnings => warnings.Ignore(eventIds: InMemoryEventId.TransactionIgnoredWarning))
.Options;
var mockDbContext = new Mock<MyDbContext>(args: dbContextOptions);
mockDbContext.Setup(x => x.Database).Returns(new MockDatabase(mockDbContext.Object));
Now when the code under test calls this.db.Database.BeginTransaction(), Moq will hand it the MockDatabase and it will run without throwing the exception.
This solution was developed using Microsoft.EntityFrameworkCore version 7.0.15
Upvotes: 0
Reputation: 1
We implemented ivaylo-pashov's solution along with this code:
//Dependency Injection
public static void RegisterTypes(IUnityContainer container)
{
// Register manager mappings.
container.RegisterType<IDatabaseContextProvider, EntityContextProvider>(new PerResolveLifetimeManager());
}
}
//Test Setup
/// <summary>
/// Mocked <see cref="IrdEntities" /> context to be used in testing.
/// </summary>
private Mock<CCMSEntities> _irdContextMock;
/// <summary>
/// Mocked <see cref="IDatabaseContextProvider" /> context to be used in testing.
/// </summary>
private Mock<IDatabaseContextProvider> _EntityContextProvider;
...
_irdContextMock = new Mock<CCMSEntities>();
_irdContextMock.Setup(m => m.Outbreaks).Returns(new Mock<DbSet<Outbreak>>().SetupData(_outbreakData).Object);
_irdContextMock.Setup(m => m.FDI_Number_Counter).Returns(new Mock<DbSet<FDI_Number_Counter>>().SetupData(new List<FDI_Number_Counter>()).Object);
_EntityContextProvider = new Mock<IDatabaseContextProvider>();
_EntityContextProvider.Setup(m => m.Context).Returns(_irdContextMock.Object);
_irdOutbreakRepository = new IrdOutbreakRepository(_EntityContextProvider.Object, _loggerMock.Object);
// Usage in the Class being tested:
//Constructor
public IrdOutbreakRepository(IDatabaseContextProvider entityContextProvider, ILogger logger)
{
_entityContextProvider = entityContextProvider;
_irdContext = entityContextProvider.Context;
_logger = logger;
}
/// <summary>
/// The wrapper for the Entity Framework context and transaction.
/// </summary>
private readonly IDatabaseContextProvider _entityContextProvider;
// The usage of a transaction that automatically gets mocked because the return type is void.
_entityContextProvider.BeginTransaction();
...
Upvotes: 0
Reputation: 479
What you need is something that you can call Commit() and Rollback() on and is shaped like a System.Data.Entity.DbContextTransaction, right? So, it turns out that you can use a real DbContextTransaction on ANY real database. Then, as long as none of your testing code is making any real changes on the database used for the transaction, the Commit() or Rollback() will succeed and do nothing.
In my application, the web api layer needs to do a couple of business logic operations inside a db transaction so that if the second operation gets an error, the first operation never happened. I added a method to my business logic interface to return a transaction that the web api layer can use. In my test of that code, I mock the method to return a DbContextTransaction on an empty test database. Here's the setup code I used:
var scope = (new PConn.DataAccess.PressConnEntities()).Database.BeginTransaction();
var bizl = new Mock<IOrderMgr>();
bizl.Setup(m => m.CreateNewOrder(7, It.IsAny<string>(), It.IsAny<string>())).Returns(_testOrder1);
// .GetOrdersQuery(channel, beginUTC, endUTC);
bizl.Setup(m => m.GetOrdersQuery(7, It.IsAny<DateTime>(), It.IsAny<DateTime>())).Returns(matchedOrdersList.AsQueryable());
bizl.Setup(m => m.BeginTransaction()).Returns(scope);
Only the first line of the code snippet and the last line are important for the problem you are trying to solve.
To summarize:
Here is an example of the code under test where I use the (not so) fake transaction:
using (var scope = this.OrderManager.BeginTransaction())
{
PrintOrder pconnOrder = this.OrderManager.CreateNewOrder(channel, payload, claimsIdentity.Name);
bool parseResult = this.OrderManager.ParseNewOrder(pconnOrder, claimsIdentity.Name, out parseErrorMessage);
if (!parseResult)
{
// return a fault to the caller
HttpResponseMessage respMsg = new HttpResponseMessage(HttpStatusCode.BadRequest);
respMsg.Content = new StringContent(parseErrorMessage);
throw (new HttpResponseException(respMsg));
}
scope.Commit();
return (pconnOrder.PrintOrderID);
}
Upvotes: 0
Reputation: 31
I have spent few hours trying to figure it out, I believed it can be done by MS Fakes directly without wrapper or new classe.
You need to do three steps:
And that all.
static void SetupDBTransaction()
{
System.Data.Entity.Fakes.ShimDbContextTransaction transaction = new System.Data.Entity.Fakes.ShimDbContextTransaction();
transaction.Commit = () => { };
transaction.Rollback = () => { };
System.Data.Entity.Fakes.ShimDatabase database = new System.Data.Entity.Fakes.ShimDatabase();
database.BeginTransactionIsolationLevel = (isolationLevel) =>{return transaction.Instance;};
System.Data.Entity.Fakes.ShimDbContext.AllInstances.DatabaseGet = (@this) => { return database.Instance; };
}
Upvotes: 3
Reputation: 147
You can wrap the context and the transaction in an Interface and then implement the interface by some provider class:
public interface IDbContextProvider
{
YourContext Context { get; set; }
DbContextTransaction DbTransaction { get; set; }
void Commit();
void Rollback();
void BeginTransaction();
void SaveChanges();
}
and then implement it:
public class EfContextProvider : IDbContextProvider
{
public EfContextProvider(YourContext context)
{
Context = context;
}
public YourContext Context { set; get; }
public DbContextTransaction DbTransaction { set; get; }
public void Commit()
{
DbTransaction.Commit();
}
public void Rollback()
{
DbTransaction.Rollback();
}
public void BeginTransaction()
{
DbTransaction=Context.Database.BeginTransaction();
}
public void SaveChanges()
{
Context.SaveChanges();
}
}
so now give your class the IDbContextProvider dependency and work with it (It has also the context inside) . Maybe substitute the using block with _contextProvider.BeginTransaction(); and then also _contextProvider.Commit(); or _contextProvider.Rollback();
Upvotes: 12
Reputation: 5133
You can represent your EF classes as POCO classes and isolate all database interactions in database adapter classes. Those adapter classes would have an interface you could mock when testing business logic.
The database operations in the adapter classes can be tested with a real database connection, but with a dedicated database and connection string for the unit tests.
So how about testing business code wrapped in transactions?
In order to isolate the business code from the database adapters, you would have to create an interface for the EF transaction scope that you can mock.
I have previously worked with a design like this, though not with EF, but with similar POCO wrapping (in pseudo C#, not syntax or sanity checked):
interface IDatabaseAdapter
{
ITransactionScope CreateTransactionScope();
}
interface ITransactionScope : IDisposable
{
void Commit();
void Rollback();
}
class EntityFrameworkTransactionScope : ITransactionScope
{
private DbContextTransaction entityTransaction;
EntityFrameworkTransactionScope(DbContextTransaction entityTransaction)
{
this.entityTransaction = entityTransaction;
}
public Commit() { entityTransaction.Commit(); }
public Rollback() { entityTransaction.Rollback(); }
public Dispose() { entityTransaction.Dispose(); }
}
class EntityFrameworkAdapterBase : IDatabaseAdapter
{
private Database database;
protected EntityFrameworkAdapterBase(Database database)
{
this.database = database;
}
public ITransactionScope CreateTransactionScope()
{
return new EntityFrameworkTransactionScope(database.BeginTransaction());
}
}
interface IIncidentDatabaseAdapter : IDatabaseAdapter
{
SaveIncident(Incident incident);
}
public EntityIncidentDatabaseAdapter : EntityFrameworkAdapterBase, IIncidentDatabaseAdapter
{
EntityIncidentDatabaseAdapter(Database database) : base(database) {}
SaveIncident(Incident incident)
{
// code for saving the incident
}
}
The above design should allow you to create unit test for entity framework operations without worrying about business logic or transaction and to create unit tests for business logic where you can mock database failures and use MOQ or similar to check that rollback is in fact called on your ITransactionScope mock. With something like the above you should be able to cover pretty much any transaction failure at any stage in the business logic that you can think of.
Of course you should supplement your unit tests with some good integration tests, since transactions can be tricky, especially tricky deadlocks can occur when used concurrently and those would be hard to catch in a mocked test.
Upvotes: 1
Reputation: 13381
Testing this kind of stuff is always complicated, but first of all you should ask yourself if you want to unit test your business logic or if you want to integration test your application.
If you want to unit test your logic, you basically shouldn't even try to mock entity framework, because you do not want to test EF, you just want to test your code, right? To do so, mock any data access object and only unit test your business logic.
But if you want to test if your data access layer works, e.g. if your code can handle all the CRUD operations you have implemented, you should do integration tests against a real database. Do not try to mock any data access objects (EF) in this case, simply run you tests against a test database or a sql-express localDB for example.
Upvotes: 20