我正在编写单元测试来测试更新EF核心实体的控制器操作。
我使用的是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("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);
}
我的问题是,当测试运行时,当我尝试更新实体时它会出错。我收到了消息:
无法跟踪实体类型'DiaryEntry'的实例,因为已经跟踪了具有相同键的此类型的另一个实例。
这段代码在现实生活中很好用。我在测试中插入后如何停止跟踪,以便生产代码中的db上下文仍然不跟踪插入的实体。
我理解将一个接口模拟到repo模式的好处但我真的很想让这种测试方法工作 - 我们将数据插入一个内存数据库然后测试它是否已在db中更新
非常感谢任何帮助。
由于
修改 我添加了测试的完整代码,以表明我使用相同的上下文来创建数据库并插入我实例化控制器的日记条目。
答案 0 :(得分:10)
问题出在设置中。您到处使用相同的dbcontext。因此,在调用update时,EF抛出异常,即已经跟踪具有相同键的实体。代码在生产中工作,因为传递给控制器DI的每个请求都会生成一个新的控制器实例。由于控制器在构造函数中也有DbContext,在相同的服务范围内,DI也会生成新的dbcontext实例。因此,您的Edit
操作始终具有新的dbcontext。如果你真的测试了你的控制器,那么你应该确保你的控制器获得一个新的dbcontext而不是已经使用过的上下文。
您应该更改GetInMemoryApplicationIdentityContext
方法以返回DbContextOptions
,然后在设置阶段,将选项存储在字段中。每当您需要dbcontext(在保存实体或创建控制器期间)时,使用存储在该字段中的选项新建DbContext。这将为您提供所需的分离,并允许您测试控制器,因为它将在生产中进行配置。
答案 1 :(得分:2)
在您的测试'Arrange'中,您创建了一个新的DiaryEntry并且没有丢弃您的DbContext。在测试的“Act”部分(这将是您的控制器操作)中,您创建了另一个DbContext实例,然后尝试更新相同的DiaryEntry。除非您手动转向跟踪(我不会这样做),EF不知道哪个上下文应该跟踪DiaryEntry。因此错误。
正确答案:如果我不得不猜测罪魁祸首似乎是'model.ToDiaryEntryDataEntity()'。在您的控制器操作中,您没有从数据库中获取实体。您传入的是该实体的所有值,但您的扩展方法是创建同一实体的新实例,这会让EF感到困惑。您的控制器操作“工作”只是因为您新创建的DiaryEntry不在DbContext中。在你的测试中它是。 - trevorc 1小时前