在单元测试中使用EF Core SqlLite时如何防止跟踪问题

16

我正在编写单元测试来测试控制器操作,该操作更新EF Core实体。

我使用SQLLite而不是模拟。

我设置了我的数据库如下:

        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;

然后像这样将一个实体添加到数据库中:

        private DiaryEntriesController _controller;
    private ApplicationDbContext _context;

    [SetUp]
    public void SetUp()
    {
        _context = TestHelperMethods.GetInMemoryApplicationIdentityContext();
        _controller = new DiaryEntriesController(_context);
    }

    [Test]
    [Ignore("https://dev59.com/llgQ5IYBdhLWcg3w1nfm")]
    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);
    }

我的控制器方法看起来像这样:

        [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);
    }

我的问题是在测试时,当我尝试更新实体时,它会出现错误。我收到以下消息:

由于已经跟踪了具有相同键的此类型的另一个实例,因此无法跟踪实体类型“DiaryEntry”的实例。

这段代码在实际生产环境中很好用。但我卡在了测试插入数据后如何停止跟踪,以使生产代码中的数据库上下文不再跟踪插入的实体。

我明白模拟接口到存储库模式的好处,但我真的希望能让这种测试方法可行——将数据插入内存数据库,然后测试是否已在数据库中更新了该数据。

非常感谢任何帮助。

谢谢。

编辑: 我添加了完整的测试代码,以显示我正在使用相同的上下文创建数据库和插入我将控制器实例化的日记条目。

2个回答

22
问题出在设置上。您在所有地方都使用了相同的dbcontext。因此,在调用更新时,EF会抛出异常,指出已经跟踪到具有相同键的实体。该代码在生产中正常工作,因为每个请求传递给控制器时,DI都会生成控制器的新实例。由于构造函数中的控制器也具有DbContext,处于同一服务范围内,DI还将生成新的dbcontext实例。因此,您的 Edit 操作始终具有新鲜的dbcontext。如果您真的要测试控制器,则应确保控制器获取全新的dbcontext,而不是已经使用过的上下文。
您应更改 GetInMemoryApplicationIdentityContext 方法以返回 DbContextOptions ,然后在设置阶段将选项存储在字段中。每当需要dbcontext(在保存实体或创建控制器期间)时,请使用存储在字段中的选项新建DbContext。这将为您提供所需的分离,并允许您测试控制器,就像它在生产中配置的那样。

好的解释 - Stanislav Machel

2
在你的测试“Arrange”中,你创建了一个新的DiaryEntry但没有处理好你的DbContext。在测试的“Act”部分(也就是你的控制器操作)中,你创建了另一个DbContext实例并尝试更新同一个DiaryEntry。除非你手动关闭跟踪(我不会这么做),否则EF不知道哪个上下文应该跟踪DiaryEntry。因此出现了错误。
正确答案:如果我要猜测罪魁祸首似乎是'model.ToDiaryEntryDataEntity()'。在你的控制器操作中,你并没有从数据库中获取实体。你传递了该实体的所有值,但扩展方法却创建了同一实体的新实例,这让EF感到困惑。你的控制器操作“起作用”的原因只是因为你新创建的DiaryEntry不在DbContext中。而在你的测试中它是存在的。- trevorc 1小时前

感谢@trevorc。我编辑了问题,以显示我在控制器中实际使用的是与测试安排部分相同的数据库上下文。抱歉最初没有表述清楚。 - Stephen Anderson
1
如果我必须猜测罪犯,那就是 'model.ToDiaryEntryDataEntity()'。在您的控制器操作中,您没有从数据库中获取实体。您正在传递该实体的所有值,但您的扩展方法正在创建同一实体的新实例,这正是混淆 EF 的原因。您的控制器操作之所以“有效”,仅因为您新创建的 DiaryEntry 不在 DbContext 中。在您的测试中,它是存在的。 - trevorc
只是另一个建议。您的控制器编辑操作应首先获取实体,然后使用您的扩展方法将任何更改映射到属性,然后更新当前上下文中的实体。 - trevorc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接