I'm writing a unit test to test a controller action that updates an EF core entity.
I am using SQLLite, rather than mocking.
I set up my database like this:
internal static ApplicationDbContext GetInMemoryApplicationIdentityContext()
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.Options;
var context = new ApplicationDbContext(options);
context.Database.EnsureCreated();
return context;
and then add an entity to the database like this:
private DiaryEntriesController _controller;
private ApplicationDbContext _context;
[SetUp]
public void SetUp()
{
_context = TestHelperMethods.GetInMemoryApplicationIdentityContext();
_controller = new DiaryEntriesController(_context);
}
[Test]
[Ignore("http://stackoverflow.com/questions/42138960/preventing-tracking-issues-when-using-ef-core-sqllite-in-unit-tests")]
public async Task EditPost_WhenValid_EditsDiaryEntry()
{
// Arrange
var diaryEntry = new DiaryEntry
{
ID = 1,
Project = new Project { ID = 1, Name = "Name", Description = "Description", Customer = "Customer", Slug = "slug" },
Category = new Category { ID = 1, Name = "Category" },
StartDateTime = DateTime.Now,
EndDateTime = DateTime.Now,
SessionObjective = "objective",
Title = "Title"
};
_context.DiaryEntries.Add(diaryEntry);
await _context.SaveChangesAsync();
var model = AddEditDiaryEntryViewModel.FromDiaryEntryDataEntity(diaryEntry);
model.Actions = "actions";
// Act
var result = await _controller.Edit(diaryEntry.Project.Slug, diaryEntry.ID, AddEditDiaryEntryViewModel.FromDiaryEntryDataEntity(diaryEntry)) as RedirectToActionResult;
// Assert
var retreivedDiaryEntry = _context.DiaryEntries.First();
Assert.AreEqual(model.Actions, retreivedDiaryEntry.Actions);
}
My controller method looks like this:
[HttpPost]
[ValidateAntiForgeryToken]
[Route("/projects/{slug}/DiaryEntries/{id}/edit", Name = "EditDiaryEntry")]
public async Task<IActionResult> Edit(string slug, int id, [Bind("ID,CategoryID,EndDate,EndTime,SessionObjective,StartDate,StartTime,Title,ProjectID,Actions,WhatWeDid")] AddEditDiaryEntryViewModel model)
{
if (id != model.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
var diaryEntryDb = model.ToDiaryEntryDataEntity();
_context.Update(diaryEntryDb);
await _context.SaveChangesAsync();
return RedirectToAction("Details", new { slug = slug, id = id });
}
ViewData["CategoryID"] = new SelectList(_context.Categories, "ID", "Name", model.CategoryID);
ViewData["ProjectID"] = new SelectList(_context.Projects, "ID", "Customer", model.ProjectID);
return View(model);
}
My problem is that when the test runs, it errors when I try to update the entity. I get the message:
The instance of entity type 'DiaryEntry' cannot be tracked because another instance of this type with the same key is already being tracked.
The code works fine in real life. I am stuck as to how to stop the tracking after my insert in the test so that the db context that is in the production code is not still tracking the inserted entity.
I understand the benefits of mocking an interface to a repo pattern but I'd really like to get this method of testing working - where we insert data into an an-memory db and then test that it has been updated in the db.
Any help would be much appreciated.
Thanks
EDIT: I added the full code of my test to show that I'm using the same context to create the database and insert the diary entry that I instantiated the controller with.
The issue is in the setup. You are using same dbcontext everywhere. Therefore while calling update, EF throws exception that entity with same key is being tracked already. The code works in production because with every request passed to controller DI generates a new instance of controller. Since controller also have DbContext in constructor, in same service scope, DI will generate new dbcontext instance too. Hence your Edit
action always have a fresh dbcontext. If you are really testing out your controller then you should make sure that your controller is getting a fresh dbcontext rather than a context which was already used.
You should change GetInMemoryApplicationIdentityContext
method to return DbContextOptions
then during setup phase, store the options in a field. Whenever you need dbcontext (during saving entity or creating controller), new up DbContext using the options stored in the field. That would give you desired separation and allow you to test your controller as it would be configured in production.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With