Как динамически выбрать DbContext для метода конечной точки API - PullRequest
0 голосов
/ 29 мая 2020

Я разработал API, который использует вспомогательный класс для получения контекста базы данных для каждой функции конечной точки. Теперь я пытаюсь написать модульные тесты для каждой конечной точки и хочу использовать базу данных в памяти в моем проекте модульного тестирования.

Проблема, с которой я столкнулся, заключается в том, что для вызова функций API мне пришлось добавить конструктор в свой класс контроллера API. Это позволило бы мне передать dbContext базы данных в памяти функции контроллера для использования. Однако с момента добавления конструктора я получил следующую ошибку при попытке попасть в конечную точку:

"exceptionMessage": "Unable to resolve service for type 'AppointmentAPI.Appt_Models.ApptSystemContext' while attempting to activate 'AppointmentAPI.Controllers.apptController'."

UPDATE

controller.cs

 public class apptController : Controller
    {
        private readonly ApptSystemContext _context;

        public apptController(ApptSystemContext dbContext)
        {
            _context = dbContext;
        }


        #region assingAppt
        /*
         * assignAppt()
         *
         * Assigns newly created appointment to slot
         * based on slotId
         *
         */
        [Authorize]
        [HttpPost]
        [Route("/appt/assignAppt")]
        public string assignAppt([FromBody] dynamic apptData)
        {
            int id = apptData.SlotId;
            string json = apptData.ApptJson;
            DateTime timeStamp = DateTime.Now;

            using (_context)
            {
               var slot = _context.AppointmentSlots.Single(s => s.SlotId == id);

                // make sure there isn't already an appointment booked in appt slot
                if (slot.Timestamp == null)
                {
                    slot.ApptJson = json;
                    slot.Timestamp = timeStamp;

                    _context.SaveChanges();
                    return "Task Executed\n";
                }
                else
                {
                    return "There is already an appointment booked for this slot.\n" +
                           "If this slot needs changing try updating it instead of assigning it.";
                }
            }
        }
   }

UnitTest.cs

using System;
using Xunit;
using AppointmentAPI.Controllers;
using AppointmentAPI.Appt_Models;
using Microsoft.EntityFrameworkCore;

namespace XUnitTest
{
    public abstract class UnitTest1
    {
        protected UnitTest1(DbContextOptions<ApptSystemContext> contextOptions)
        {
            ContextOptions = contextOptions;

            SeedInMemoryDB();
        }

        protected DbContextOptions<ApptSystemContext> ContextOptions { get; }

        private void SeedInMemoryDB()
        {
            using(var context = new ApptSystemContext(ContextOptions))
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();

                var seventh = new AppointmentSlots
                {
                    SlotId = 7,
                    Date = Convert.ToDateTime("2020-05-19 00:00:00.000"),
                    Time = TimeSpan.Parse("08:45:00.0000000"),
                    ApptJson = null,
                    Timestamp = null
                };

                context.AppointmentSlots.Add(seventh);
                context.SaveChanges();
            }
        }

        [Fact]
        public void Test1()
        {
            DbContextOptions<ApptSystemContext> options;
            var builder = new DbContextOptionsBuilder<ApptSystemContext>();
            builder.UseInMemoryDatabase();
            options = builder.Options;

            var context = new ApptSystemContext(options);

            var controller = new apptController(context);

            // Arrange
            var request = new AppointmentAPI.Appt_Models.AppointmentSlots
            {
                SlotId = 7,
                ApptJson = "{'fname':'Emily','lname':'Carlton','age':62,'caseWorker':'Brenda', 'appStatus':'unfinished'}",
                Timestamp = Convert.ToDateTime("2020-06-25 09:34:00.000")
            };

            string expectedResult = "Task Executed\n";

            // Act
            var response = controller.assignAppt(request);

            Assert.Equal(response, expectedResult);

        }
    }
}

InMemoryClass.cs

using System;
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using AppointmentAPI.Appt_Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace XUnitTest
{
    public class InMemoryClass1 : UnitTest1, IDisposable
    {
        private readonly DbConnection _connection;

        public InMemoryClass1()
            :base(
                 new DbContextOptionsBuilder<ApptSystemContext>()
                    .UseSqlite(CreateInMemoryDB())
                    .Options
            )
        {
            _connection = RelationalOptionsExtension.Extract(ContextOptions).Connection;
        }

        private static DbConnection CreateInMemoryDB()
        {
            var connection = new SqliteConnection("DataSource=:memory:");

            connection.Open();

            return connection;
        }

        public void Dispose() => _connection.Dispose();

    }
}

1 Ответ

1 голос
/ 29 мая 2020

Исключение предполагает, что вы не зарегистрировали свой DBContext в своем Startup.cs (как упоминалось выше). Я также предлагаю вам изменить имя вашего частного свойства только для чтения на что-то другое, кроме DbContext (которое является именем класса и может запутать). Используйте что-то вроде этого:

private readonly ApptSystemContext _context;

Кроме того, ваш подход следует изменить.

Во-первых, вы установите строку подключения при регистрации DBContext. Просто позвольте инъекции зависимостей позаботиться об этом за вас. Ваш контроллер должен выглядеть так:

        public apptController(ApptSystemContext dbContext)
        {
            _context = dbContext;
        }

dbContext не будет нулевым, если вы зарегистрируете его в Startup.

Далее, модульное тестирование - сложная концепция, но как только вы напишете свой Модульный тест, вы начнете понимать немного лучше.

Вы сказали, что хотите использовать базу данных SQL In Memory для модульного тестирования, что является хорошим подходом (имейте в виду, что есть ограничения до SQL В Mem, как без ограничений FK). Затем я предполагаю, что вы хотите протестировать свой контроллер, поэтому, поскольку вы ДОЛЖНЫ передать DBContext, чтобы создать экземпляр своего контроллера, вы можете создать новый экземпляр DBContext, который настроен для использования базы данных в памяти.

Например,

public void ApptControllerTest()
{
   //create new dbcontext
   DbContextOptions<ApptSystemContext> options;
        var builder = new DbContextOptionsBuilder<ApptSystemContext>();
        builder.UseInMemoryDatabase();
        options = builder.Options;

    var context = new ApptSystemContext(options);

   //instantiate your controller
   var controller = new appController(context);

   //call your method that you want to test
   var retVal = controller.assignAppt(args go here);
}

Измените тело метода на это:

public string assignAppt([FromBody] dynamic apptData)
        {
            int id = apptData.SlotId;
            string json = apptData.ApptJson;
            DateTime timeStamp = DateTime.Now;

            using (_context)
            {
               var slot = _context.AppointmentSlots.Single(s => s.SlotId == id);

                // make sure there isn't already an appointment booked in appt slot
                if (slot.Timestamp == null)
                {
                    slot.ApptJson = json;
                    slot.Timestamp = timeStamp;

                    _context.SaveChanges();
                    return "Task Executed\n";
                }
                else
                {
                    return "There is already an appointment booked for this slot.\n" +
                           "If this slot needs changing try updating it instead of assigning it.";
                }
            }
        }

Еще одно предложение: не используйте объект Dynami c в качестве тела запроса, если вы не абсолютно вынуждены это сделать. Использование объекта Dynami c позволяет передавать что угодно, и вы теряете возможность определять, является ли запрос приемлемым или нет.

...