#c# #entity-framework #unit-testing #mocking
#c# #entity-framework #модульное тестирование #издевательство
Вопрос:
Я использую базу данных InMemoryDatabase для выполнения некоторых Post и Gets для моего приложения react. Я только начал писать модульные тесты, но один конкретный (Post_Id_WorkEntry_shouldReturn_Ok) доставляет мне трудности, потому что я получаю ошибку такого типа: The instance of entity type ' Project ' cannot be tracked because another instance with the same key value for {' Id '} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using ' DbContextOptionsBuilder.EnableSensitiveDataLogging ' to see the conflicting key values.
Мой контроллер:
namespace Timelogger.Api.Controllers
{
[Route("api/[controller]")]
public class ProjectsController : Controller
{
private readonly ApiContext _context;
public ProjectsController(ApiContext context)
{
_context = context;
}
[HttpGet]
[Route("HelloWorld")]
public string HelloWorld()
{
return "Hello Back!";
}
// GET api/projects
[HttpGet]
public IActionResult Get() //async
{
return Ok(_context.Projects.Include(x => x.WorkEntries));
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] Project prj)
{
if (ModelState.IsValid)
{
try
{
Project newProject = new Project(prj.Name, prj.StartDate, prj.EndDate);
switch(newProject.ValidateProject())
{
case ProjectStatus.INVALID_DATE:
return BadRequest("Invalid dates");
case ProjectStatus.SUCCES:
_context.Projects.Add(newProject);
await _context.SaveChangesAsync();
return Ok("Added new project with ID=" prj.Id);
}
}
catch (Exception e)
{
return BadRequest(e.Message);
}
}
return BadRequest("Invalid json format");
}
//[HttpPut("{id}")]
[HttpPost]
[Route("{Id:int}/WorkEntry")]
public async Task<IActionResult> Post(int id, [FromBody] WorkEntry workEntry)
{
if (ModelState.IsValid)
{
try
{
Console.Write("WorkEntry====: " workEntry.ToString());
var updatedProject = _context.Projects.AsNoTracking().Include(x => x.WorkEntries).FirstOrDefault(x => x.Id == id);
switch (updatedProject.ValdidateAndMergeWorkEntry(workEntry))
{
case ValidStatus.SUCCES_NEW_WORKENTRY_ADDED:
Console.WriteLine("SUCCES_NEW_WORKENTRY_ADDED");
_context.Projects.Update(updatedProject);
await _context.SaveChangesAsync();
return Ok("Work entry added for project with ID=" updatedProject.Id);
case ValidStatus.SUCCES_HOURS_MERGED:
Console.WriteLine("SUCCES_HOURS_MERGED");
_context.Projects.Update(updatedProject);
await _context.SaveChangesAsync();
return Ok("Work entry merged for project with ID=" updatedProject.Id);
case ValidStatus.INVALID_HOURS_ALREADY_BOOKED:
Console.WriteLine("ERROR_HOURS_ALREADY_BOOKED");
return BadRequest("Hours are already over the legal meeting");
case ValidStatus.INVALID_DAY:
Console.WriteLine("INVALID_DAY");
return BadRequest("Day is not valid");
case ValidStatus.INVALID_HOURS:
Console.WriteLine("INVALID_HOURS");
return BadRequest("Hours are not valid");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return BadRequest(e.Message);
}
}
return BadRequest("Invalid json format");
}
}
}
И это мой UT
namespace Timelogger.Api.Tests
{
public class ProjectsControllerTests
{
ProjectsSeedDataFixture psdf = null;
[SetUp]
public void Init()
{
psdf = new ProjectsSeedDataFixture();
}
[TearDown]
public void CleanUp()
{
psdf.ProjectsContext.Database.EnsureDeleted();
}
[Test]
public void HelloWorld_ShouldReply_HelloBack()
{
//arrange
ProjectsController sut = new ProjectsController(psdf.ProjectsContext);
//act
var actual = sut.HelloWorld();
//assert
Assert.AreEqual("Hello Back!", actual);
}
[Test]
public void Get_shouldReturn_Ok()
{
//arrange
ProjectsController sut = new ProjectsController(psdf.ProjectsContext);
var testProject1 = new Project("e-conomic Interview", new System.DateTime(2019, 12, 1), new System.DateTime(2020, 12, 10));
Stubs.PopulateWorkEntries(testProject1);
psdf.ProjectsContext.Projects.Add(testProject1);
psdf.ProjectsContext.SaveChanges();
//act
var actual = sut.Get();
var okResult = actual as OkObjectResu<
//assert
Assert.IsNotNull(okResult);
Assert.AreEqual(200, okResult.StatusCode);
}
[Test]
public void Post_shouldReturn_Ok()
{
//arrange
ProjectsController sut = new ProjectsController(psdf.ProjectsContext);
var testProject1 = new Project("e-conomic Interview", new System.DateTime(2019, 12, 1), new System.DateTime(2020, 12, 10));
Stubs.PopulateWorkEntries(testProject1);
//act
var actual = sut.Post(testProject1);
var okResult = actual.Result as OkObjectResu<
//assert
Assert.IsNotNull(okResult);
Assert.AreEqual(200, okResult.StatusCode);
//Assert.AreEqual("Added new project with ID=1", okResult.Value);
}
[Test]
public void Post_shouldReturn_BadRequest()
{
//arrange
ProjectsController sut = new ProjectsController(psdf.ProjectsContext);
var testProject1 = new Project("e-conomic Interview", new System.DateTime(2020, 12, 10), new System.DateTime(2019, 12, 1));
Stubs.PopulateWorkEntries(testProject1);
//act
var actual = sut.Post(testProject1);
var badRequestResult = actual.Result as BadRequestObjectResu<
//assert
Assert.IsNotNull(badRequestResult);
Assert.AreEqual(400, badRequestResult.StatusCode);
Assert.AreEqual("Invalid dates", badRequestResult.Value);
}
[Test]
public void Post_Id_WorkEntry_shouldReturn_Ok()
{
//arrange
ProjectsController sut = new ProjectsController(psdf.ProjectsContext);
psdf.ProjectsContext.Database.EnsureDeleted();
var project = new Project("e-conomic Interview", new System.DateTime(2019, 12, 1), new System.DateTime(2020, 12, 10));
psdf.ProjectsContext.Projects.Add(project);
psdf.ProjectsContext.SaveChanges();
WorkEntry workEntry = new WorkEntry { id = project.WorkEntries.Count 1, Day = new System.DateTime(2019, 12, 1), Hours = 3 };
//act
var actual = sut.Post(1 , workEntry);
var okResult = actual.Result as OkObjectResu<
//assert
Assert.IsNotNull(okResult);
Assert.AreEqual(200, okResult.StatusCode);
}
}
}
Есть идеи, как я могу сделать post тестируемым? Это потому, что контекст из post отличается от контекста из UT? есть ли возможность издеваться над ним?
Обновить:
Вот как сейчас выглядит мой тестовый класс
using Timelogger.Api.Controllers;
using NUnit.Framework;
using System;
using Microsoft.AspNetCore.Mvc;
using Timelogger.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Timelogger.Api.Tests
{
[TestFixture]
public class ProjectsControllerTests
{
private static DbContextOptions<ApiContext> CreateNewContextOptions()
{
// Create a fresh service provider, and therefore a fresh
// InMemory database instance.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Create a new options instance telling the context to use an
// InMemory database and the new service provider.
var builder = new DbContextOptionsBuilder<ApiContext>().UseInMemoryDatabase("data").UseInternalServiceProvider(serviceProvider);
return builder.Options;
}
[Test]
public void HelloWorld_ShouldReply_HelloBack()
{
using (var context = new ApiContext(CreateNewContextOptions()))
{
//arrange
ProjectsController sut = new ProjectsController(context);
//act
var actual = sut.HelloWorld();
//assert
Assert.AreEqual("Hello Back!", actual);
};
}
[Test]
public void Get_shouldReturn_Ok()
{
using (var context = new ApiContext(CreateNewContextOptions()))
{
//arrange
ProjectsController sut = new ProjectsController(context);
var testProject1 = new Project("e-conomic Interview", new System.DateTime(2019, 12, 1), new System.DateTime(2020, 12, 10));
Stubs.PopulateWorkEntries(testProject1);
context.Projects.Add(testProject1);
context.SaveChanges();
//act
var actual = sut.Get();
var okResult = actual as OkObjectResu<
//assert
Assert.IsNotNull(okResult);
Assert.AreEqual(200, okResult.StatusCode);
};
}
[Test]
public void Post_shouldReturn_Ok()
{
using(var context = new ApiContext(CreateNewContextOptions()))
{
//arrange
ProjectsController sut = new ProjectsController(context);
var testProject1 = new Project("e-conomic Interview", new System.DateTime(2019, 12, 1), new System.DateTime(2020, 12, 10));
Stubs.PopulateWorkEntries(testProject1);
//act
var actual = sut.Post(testProject1);
var okResult = actual.Result as OkObjectResu<
//assert
Assert.IsNotNull(okResult);
Assert.AreEqual(200, okResult.StatusCode);
Assert.AreEqual("Added new project with ID=1", okResult.Value);
};
}
[Test]
public void Post_shouldReturn_BadRequest()
{
using (var context = new ApiContext(CreateNewContextOptions()))
{
//arrange
ProjectsController sut = new ProjectsController(context);
var testProject1 = new Project("e-conomic Interview", new System.DateTime(2020, 12, 10), new System.DateTime(2019, 12, 1));
Stubs.PopulateWorkEntries(testProject1);
//act
var actual = sut.Post(testProject1);
var badRequestResult = actual.Result as BadRequestObjectResu<
//assert
Assert.IsNotNull(badRequestResult);
Assert.AreEqual(400, badRequestResult.StatusCode);
Assert.AreEqual("Invalid dates", badRequestResult.Value);
};
}
[Test]
public void PostIdWorkEntry_shouldReturn_Ok()
{
using (var context = new ApiContext(CreateNewContextOptions()))
{
//arrange
ProjectsController sut = new ProjectsController(context);
var project = new Project("e-conomic Interview", new System.DateTime(2019, 12, 1), new System.DateTime(2020, 12, 10));
context.Projects.Add(project);
context.SaveChanges();
WorkEntry workEntry = new WorkEntry(new System.DateTime(2019, 12, 3), 3 );
//act
var actual = sut.Post(project.Id, workEntry);
var okResult = actual.Result as OkObjectResu<
//assert
Assert.IsNotNull(okResult);
Assert.AreEqual(200, okResult.StatusCode);
};
}
[Test]
public void PostIdWorkEntry_shouldReturn_BadRequestInvalidHoursSurpassed()
{
using (var context = new ApiContext(CreateNewContextOptions()))
{
//arrange
ProjectsController sut = new ProjectsController(context);
var project = new Project("e-conomic Interview", new System.DateTime(2019, 12, 1), new System.DateTime(2020, 12, 10));
WorkEntry workEntry1 = new WorkEntry { id = project.WorkEntries.Count 1, Day = new System.DateTime(2019, 12, 1), Hours = 7 };
project.WorkEntries.Add(workEntry1);
context.Projects.Add(project);
context.SaveChanges();
WorkEntry workEntry2 = new WorkEntry { id = project.WorkEntries.Count 1, Day = new System.DateTime(2019, 12, 1), Hours = 2 };
//act
var actual = sut.Post(project.Id, workEntry2);
var badRequestResult = actual.Result as BadRequestObjectResu<
//assert
Assert.IsNotNull(badRequestResult);
Assert.AreEqual(400, badRequestResult.StatusCode);
Assert.AreEqual("Hours are over the legal point", badRequestResult.Value);
};
}
[Test]
public void PostIdWorkEntry_shouldReturn_BadRequestInvalidDay()
{
using (var context = new ApiContext(CreateNewContextOptions()))
{
//arrange
ProjectsController sut = new ProjectsController(context);
var project = new Project("e-conomic Interview", new System.DateTime(2019, 12, 1), new System.DateTime(2020, 12, 10));
context.Projects.Add(project);
context.SaveChanges();
WorkEntry workEntry = new WorkEntry { id = project.WorkEntries.Count 1, Day = new System.DateTime(2020, 12, 11), Hours = 2 };
//act
var actual = sut.Post(project.Id, workEntry);
var badRequestResult = actual.Result as BadRequestObjectResu<
//assert
Assert.IsNotNull(badRequestResult);
Assert.AreEqual(400, badRequestResult.StatusCode);
Assert.AreEqual("Day is not valid", badRequestResult.Value);
};
}
}
}
Ответ №1:
Я думаю, что контекст EF отслеживает проект, который вы добавили в метод тестирования, затем, когда ваш контроллер извлекает его из базы данных из-за AsNoTracking, он не видит, что это тот же проект, который он отслеживает.
Вы могли бы попробовать использовать два разных контекста, таким образом, контекст, который вы передаете своему контроллеру, не будет отслеживать проект.
Вот как я использую базу данных в памяти для тестирования:
var options = new DbContextOptionsBuilder<ApiContext>()
.UseLazyLoadingProxies(fixture.IsLazyLoading)
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
Seedcontext = new ApiContext(options);
Context = new ApiContext(options);
Resultcontext = new ApiContext(options);
Seedcontext.Database.EnsureCreated();
Initializer.Seed(Seedcontext);
Каждый контекст получает доступ к одной и той же БД, но отслеживание ограничено каждым отдельным контекстом.
Комментарии:
1. используя ваши входные данные, я должен решить свою первоначальную проблему, но теперь кажется, что есть проблема с обновлением или удалением. Майкрософт. EntityFrameworkCore. Исключение DbUpdateConcurrencyException: попытка обновить или удалить объект, которого нет в хранилище. это исключение. Дело в том, что с помощью отладчика я вижу, что объект updatedProject получает обновление, но он выходит из строя при выполнении SaveChanges
2. Потому что вы удалили базу данных с помощью вызова ensuredeleted. При этом база данных удаляется, поэтому таблиц для обновления больше не существует. Попробуйте удалить эту строку из вашего теста. Чего вы пытались достичь с его помощью?
3. .. также, если ваша база данных была заполнена, вам не нужно добавлять проект. Вы просто хотите протестировать добавление workentry, верно?
4. это был остаток, но в текущем коде я не выполняю никаких вызовов ensuredeleted, и поведение остается прежним. Ниже я опубликую обновленную версию своего кода.
5. Что, если удалить asnotracking из контроллера?
Ответ №2:
Пожалуйста, убедитесь, что идентификатор таблицы добавляется автоматически (автоматическое увеличение позволяет автоматически генерировать уникальный номер при вставке новой записи в таблицу)
Ответ №3:
Лучше всего было бы удалить контекст и зарегистрировать его снова.
Но попробуйте вызвать
_context.ChangeTracker.Clear();
также, если вы не используете Sqlite
, который не поддерживает это, вы можете вызвать
_context.EnsureDeleted()
_context.EnsureCreated()