Stephen Anderson
Stephen Anderson

Reputation: 390

Preventing tracking issues when using EF Core SqlLite in Unit Tests

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.

Upvotes: 16

Views: 9478

Answers (2)

Smit
Smit

Reputation: 2459

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.

Upvotes: 22

trevorc
trevorc

Reputation: 3031

In your test 'Arrange' you have created a new DiaryEntry and not disposed of your DbContext. In the 'Act' portion of your test (which would be your controller action) you have created another instance of DbContext and then try and update the same DiaryEntry. Unless you manually turn of tracking (which I would not do) EF doesn't know which context should track the DiaryEntry. Hence the error.

Correct answer: If I had to guess the culprit appears to be 'model.ToDiaryEntryDataEntity()'. In your controller action you aren't getting the entity from the database. You are passing in all of the values of that entity but your extension method is creating a new instance of the same entity which is what is confusing EF. Your controller action 'worked' only because your newly created DiaryEntry was not in the DbContext. In your test it is. – trevorc 1 hour ago

Upvotes: 2

Related Questions