在单元测试中使用EF Core SqlLite时防止跟踪问题 [英] Preventing tracking issues when using EF Core SqlLite in Unit Tests

查看:87
本文介绍了在单元测试中使用EF Core SqlLite时防止跟踪问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在编写一个单元测试,以测试更新EF核心实体的控制器动作.

I'm writing a unit test to test a controller action that updates an EF core entity.

我使用的是SQLLite,而不是模拟.

I am using SQLLite, rather than mocking.

我这样设置数据库:

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

我的控制器方法如下:

        [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:

无法跟踪实体类型'DiaryEntry'的实例,因为已经跟踪了具有相同键的该类型的另一个实例.

The instance of entity type 'DiaryEntry' cannot be tracked because another instance of this type with the same key is already being tracked.

该代码在现实生活中可以正常工作.我对如何在插入测试后停止跟踪感到困惑,以使生产代码中的db上下文仍无法跟踪插入的实体.

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.

任何帮助将不胜感激.

谢谢

我添加了测试的完整代码,以表明我使用相同的上下文创建数据库并插入实例化控制器的日记条目.

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.

推荐答案

问题出在安装程序中.您到处都使用相同的dbcontext.因此,在调用update时,EF引发异常,即已跟踪具有相同密钥的实体.该代码在生产环境中有效,因为传递给控制器​​DI的每个请求都会生成一个新的控制器实例.由于控制器在构造函数中也具有DbContext,因此在相同的服务范围内,DI也将生成新的dbcontext实例.因此,您的Edit操作始终具有新的dbcontext.如果您真的在测试控制器,则应确保控制器获得的是新的dbcontext,而不是已经使用的上下文.

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.

您应该更改GetInMemoryApplicationIdentityContext方法以返回DbContextOptions,然后在设置阶段将选项存储在字段中.每当您需要dbcontext时(在保存实体或创建控制器期间),请使用字段中存储的选项来新建DbContext.这样可以实现理想的分离,并允许您像在生产环境中配置的那样测试控制器.

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.

这篇关于在单元测试中使用EF Core SqlLite时防止跟踪问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆