Переконфигурировать зависимости при тестировании интеграции ASP.NET Core Web API и EF Core

Я следую этому уроку
Интеграционное тестирование с Entity Framework Core и SQL Server

Мой код выглядит так

Интеграционный тестовый класс

public class ControllerRequestsShould : IDisposable
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    private readonly YourContext _context;

    public ControllerRequestsShould()
    {
        // Arrange
        var serviceProvider = new ServiceCollection()
            .AddEntityFrameworkSqlServer()
            .BuildServiceProvider();

        var builder = new DbContextOptionsBuilder<YourContext>();

        builder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=your_db_{Guid.NewGuid()};Trusted_Connection=True;MultipleActiveResultSets=true")
            .UseInternalServiceProvider(serviceProvider);

        _context = new YourContext(builder.Options);
        _context.Database.Migrate();

        _server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>()
            .UseEnvironment(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")));
        _client = _server.CreateClient();
    }

    [Fact]
    public async Task ReturnListOfObjectDtos()
    {
        // Arrange database data
        _context.ObjectDbSet.Add(new ObjectEntity{ Id = 1, Code = "PTF0001", Name = "Portfolio One" });
        _context.ObjectDbSet.Add(new ObjectEntity{ Id = 2, Code = "PTF0002", Name = "Portfolio Two" });

        // Act
        var response = await _client.GetAsync("/api/route");
        response.EnsureSuccessStatusCode();


        // Assert
        var result = Assert.IsType<OkResult>(response);            
    }

    public void Dispose()
    {
        _context.Dispose();
    }

Насколько я понимаю, метод .UseStartUp гарантирует, что TestServer использует мой класс запуска

У меня проблема в том, что, когда мой закон акт

var response = await _client.GetAsync("/api/route");

Я получаю сообщение об ошибке в моем классе запуска, что строка подключения пуста. Я думаю, что мое понимание проблемы состоит в том, что, когда мой контроллер получает доступ от клиента, он внедряет мой репозиторий данных, который, в свою очередь, вводит контекст БД.

Я думаю, что мне нужно настроить сервис как часть new WebHostBuilder раздела new WebHostBuilder чтобы он использовал контекст, созданный в тесте. Но я не уверен, как это сделать.

Метод ConfigureServices в Startup.cs

        public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services
        services.AddMvc(setupAction =>
        {
            setupAction.ReturnHttpNotAcceptable = true;
            setupAction.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
            setupAction.InputFormatters.Add(new XmlDataContractSerializerInputFormatter());
        });

        // Db context configuration
        var connectionString = Configuration["ConnectionStrings:YourConnectionString"];
        services.AddDbContext<YourContext>(options => options.UseSqlServer(connectionString));

        // Register services for dependency injection
        services.AddScoped<IYourRepository, YourRepository>();
    }

Ответ 1

Вот два варианта:

1. Используйте WebHostBuilder.ConfigureServices

Через некоторое время я думаю, что самое простое решение - использовать WebHostBuilder.ConfigureServices вместе с WebHostBuilder.UseStartup<T> чтобы переопределить и WebHostBuilder.UseStartup<T> регистрации DI веб-приложения:

_server = new TestServer(new WebHostBuilder()
    .ConfigureServices(services =>
    {
        services.AddScoped<IFooService, MockService>();
    })
    .UseStartup<Startup>()
);

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        //use TryAdd to support mocking the service
        services.TryAddTransient<IFooService, FooService>();
    }
}

Ключевым моментом здесь является использование методов TryAdd внутри исходного класса Startup. Пользовательское WebHostBuilder.ConfigureServices вызывается перед Startup, так что вы зарегистрировать издеваетесь, прежде чем реальные услуги. TryAdd пропускает регистрацию, если тот же интерфейс уже зарегистрирован, поэтому реальные сервисы не будут установлены.

Дополнительная информация: Запуск интеграционных тестов для основных приложений ASP.NET.

2. Наследование/новый класс Startup

Создайте класс TestStartup для повторной настройки ASP.NET Core DI. Вы можете унаследовать его от Startup и переопределить только необходимые методы:

public class TestStartup : Startup
{
    public TestStartup(IHostingEnvironment env) : base(env) { }

    public override void ConfigureServices(IServiceCollection services)
    {
        //mock DbContext and any other dependencies here
    }
}

Кроме того, TestStartup может быть создан с нуля, чтобы сохранить TestStartup тестирования.

И укажите его в UseStartup для запуска тестового сервера:

_server = new TestServer(new WebHostBuilder().UseStartup<TestStartup>());

Полный пример: интеграционное тестирование вашего основного приложения asp.net с базой данных в памяти.

Ответ 2

@ilya-chumakov ответ потрясающий. Я просто хотел бы добавить еще один вариант

3. Используйте метод ConfigureTestServices из WebHostBuilderExtensions.

Метод ConfigureTestServices доступен в Microsoft.AspNetCore.TestHost версии 2.1 (20.05.2018 это RC1-финал). И это позволяет нам переопределять существующие регистрации с помощью макетов.

Код:

_server = new TestServer(new WebHostBuilder()
    .UseStartup<Startup>()
    .ConfigureTestServices(services =>
    {
        services.AddTransient<IFooService, MockService>();
    })
);