Reputation: 632
I am using EF Core 6.0.x + NUnit + Moq. Below example is strongly anonymized, the real scenario actually makes sense.
I have a DbContext:
public class MyDbContext : DbContext
{
public virtual DbSet<Foo> Foos { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }
public virtual void PreSaveActions()
=> throw new NotImplementedException(); //Here I've got something that must be done pre-save.
public override int SaveChanges()
{
PreSaveActions();
return base.SaveChanges();
}
}
I have a method similar to this:
public class SafeRemover
{
private readonly IDbContextFactory<MyDbContext> _contextFactory;
public SafeRemover(IDbContextFactory<MyDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public int SafeRemove(string name, int barId)
{
using var context = _contextFactory.CreateDbContext();
var itemToRemove = context.Foos.SingleOrDefault(foo => foo.BarId == barId && foo.Name == name);
if (itemToRemove != null)
context.Foos.Remove(itemToRemove);
return context.SaveChanges();
}
}
DependencyInjection registrations:
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.ConfigureServices((_, services) =>
{
services
.AddDbContextFactory<MyDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("MyDbContext")))
.AddSingleton<SafeRemover>();
}).Build();
I want to unit test that this method removes or not certain entities from the given set in database context.
private static readonly object[] _safeRemoveSource =
{
new TestCaseData("Foo1", 1, new List<Foo>()).SetName("SafeRemove_FooExists_FooRemoved"),
new TestCaseData("Foo2", 1, new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } }).SetName("SafeRemove_FooWithDifferentNameExists_FooNotRemoved"),
new TestCaseData("Foo1", 2, new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } }).SetName("SafeRemove_FooWithDifferentBarIdExists_FooNotRemoved"),
}
[TestCaseSource(nameof(_safeRemoveSource))]
public void SafeRemoveTest(string name, int barId, IList<Foo> expectedFoos)
{
var options = new DbContextOptionsBuilder<MyDbContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var contextMock = new Mock<MyDbContext>(options) {CallBase = true};
contextMock.Setup(context => context.PreSaveActions());
var contextFactoryMock = new Mock<IDbContextFactory<MyDbContext>>();
contextFactoryMock.Setup(factory => factory.CreateDbContext()).Returns(contextMock.Object);
var safeRemover = new SafeRemover(contextFactoryMock.Object);
safeRemover.SafeRemove(name, barId);
var actualFoos = contextMock.Object.Foos.ToList();
Assert.AreEqual(expectedFoos.Count(), actualFoos.Count());
for (var i = 0; i < expectedFoos.Count(); i++)
Assert.That(expectedFoos[i].Name.Equals(actualFoos[i].Name) && expectedFoos[i].BarId == actualFoos[i].BarId);
}
When I use the InMemoryDatabase
I am unable to check the value of contextMock.Object.Foos.ToList()
after invoking saveRemover.SafeRemove(name, barId)
because the contextMock.Object
is already disposed:
System.ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur is you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'Context'.
private static readonly object[] _safeRemoveSource =
{
new TestCaseData("Foo1", 1, new List<Foo>()).SetName("SafeRemove_FooExists_FooRemoved"),
new TestCaseData("Foo2", 1, new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } }).SetName("SafeRemove_FooWithDifferentNameExists_FooNotRemoved"),
new TestCaseData("Foo1", 2, new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } }).SetName("SafeRemove_FooWithDifferentBarIdExists_FooNotRemoved"),
}
[TestCaseSource(nameof(_safeRemoveSource))]
public void SafeRemoveTest(string name, int barId, IList<Foo> expectedFoos)
{
var actualFoos = new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } };
var options = new DbContextOptionsBuilder<MyDbContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var foosMock = new Mock<DbSet<Foo>> {CallBase = true};
foosMock.Setup(set => set.Remove(It.IsAny<Foo>())).Callback<Foo>(foo => actualFoos.Remove(foo));
var contextMock = new Mock<MyDbContext>(options) {CallBase = true};
contextMock.Setup(context => context.PreSaveActions());
contextMock.Setup(context => context.Foos).Returns(foosMock.Object);
var contextFactoryMock = new Mock<IDbContextFactory<MyDbContext>>();
contextFactoryMock.Setup(factory => factory.CreateDbContext()).Returns(contextMock.Object);
var safeRemover = new SafeRemover(contextFactoryMock.Object);
safeRemover.SafeRemove(name, barId);
Assert.AreEqual(expectedFoos.Count(), actualFoos.Count());
for (var i = 0; i < expectedFoos.Count(); i++)
Assert.That(expectedFoos[i].Name.Equals(actualFoos[i].Name) && expectedFoos[i].BarId == actualFoos[i].BarId);
}
Everything seems to work as expected instead of one thing... I'm getting NotSupportedException on SingleOrDefault
method in above configuration of mocks:
System.NotSupportedException : Specified method is not supported.
I went back to the attempt 1 and removed using
clause. Now it seems to work as expected, but am I safe to do so? Would DI container be smart enough to leave it entirely for him?
I can't find anything about that in the docs and I'm not that good at profiling to check it comprehensively.
I've used InMemoryDatabase with derivered TestMyDbContext
class. Still without success. I've got ObjectDisposedException
on the following line: var actualFoos = context.Foos.ToList();
TestMyDbContext:
public class TestMyDbContext : MyDbContext
{
public override void PreSaveActions() {}
}
Unit test:
private static readonly object[] _safeRemoveSource =
{
new TestCaseData("Foo1", 1, new List<Foo>()).SetName("SafeRemove_FooExists_FooRemoved"),
new TestCaseData("Foo2", 1, new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } }).SetName("SafeRemove_FooWithDifferentNameExists_FooNotRemoved"),
new TestCaseData("Foo1", 2, new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } }).SetName("SafeRemove_FooWithDifferentBarIdExists_FooNotRemoved"),
}
[TestCaseSource(nameof(_safeRemoveSource))]
public void SafeRemoveTest(string name, int barId, IList<Foo> expectedFoos)
{
var initialFoos = new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } };
var options = new DbContextOptionsBuilder<MyDbContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
using var context = new TestMyDbContext(options);
context.Foos.AddRange(actualFoos);
context.SaveChanges();
var contextFactoryMock = new Mock<IDbContextFactory<MyDbContext>>();
contextFactoryMock.Setup(factory => factory.CreateDbContext()).Returns(context);
var safeRemover = new SafeRemover(contextFactoryMock.Object);
safeRemover.SafeRemove(name, barId);
var actualFoos = context.Foos.ToList();
Assert.AreEqual(expectedFoos.Count(), actualFoos.Count());
for (var i = 0; i < expectedFoos.Count(); i++)
Assert.That(expectedFoos[i].Name.Equals(actualFoos[i].Name) && expectedFoos[i].BarId == actualFoos[i].BarId);
}
Upvotes: 1
Views: 4709
Reputation: 632
I've got an answer from one of NUnit collaborator under this thread: https://github.com/nunit/nunit/issues/4090
Long story short - make sure that you are creating new DbContext instance each time when it's needed and just reuse the "options" to point out on which InMemory database should they operate:
private static readonly object[] _safeRemoveSource =
{
new TestCaseData("Foo1", 1, new List<Foo>()).SetName("SafeRemove_FooExists_FooRemoved"),
new TestCaseData("Foo2", 1, new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } }).SetName("SafeRemove_FooWithDifferentNameExists_FooNotRemoved"),
new TestCaseData("Foo1", 2, new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } }).SetName("SafeRemove_FooWithDifferentBarIdExists_FooNotRemoved"),
}
[TestCaseSource(nameof(_safeRemoveSource))]
public void SafeRemoveTest(string name, int barId, IList<Foo> expectedFoos)
{
var initialFoos = new List<Foo> { new Foo { Name = "Foo1", BarId = 1 } };
var options = new DbContextOptionsBuilder<MyDbContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
using var context = new TestMyDbContext(options);
context.Foos.AddRange(actualFoos);
context.SaveChanges();
var contextFactoryMock = new Mock<IDbContextFactory<MyDbContext>>();
contextFactoryMock.Setup(factory => factory.CreateDbContext()).Returns(new TestMyDbContext(options));
var safeRemover = new SafeRemover(contextFactoryMock.Object);
safeRemover.SafeRemove(name, barId);
var actualFoos = new TestMyDbContext(options).Foos.ToList();
Assert.AreEqual(expectedFoos.Count(), actualFoos.Count());
for (var i = 0; i < expectedFoos.Count(); i++)
Assert.That(expectedFoos[i].Name.Equals(actualFoos[i].Name) && expectedFoos[i].BarId == actualFoos[i].BarId);
}
Upvotes: 0