Модульный тест C # для метода POST, который использует контекст БД с inMemoryDb

#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()