Переконфигурируйте зависимости при тестировании интеграции 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 раздел, чтобы он использовал контекст, созданный в тесте. Но я не уверен, как это сделать.

Метод 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>();
    }

3 ответа

Ответ @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>();
    })
);

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

Через некоторое время я думаю, что самым простым решением является использование WebHostBuilder.ConfigureServices вместе с 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 может быть создан с нуля, чтобы держать тестирование чище.

И указать это в UseStartup запустить тестовый сервер:

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

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

@Илья-Чумаков @lilo.jacob, оба ответа находятся на правильном пути, но я хотел бы добавить последнее дополнение, которое сделало бы ответ совершенно идеальным.

Если у вас есть

      .ConfigureServices(services =>
                   {
                       serviceCollection
                      .Replace(ServiceDescriptor.Scoped(typeof(IFooService), a => MockService));
                   }
               )

Таким образом, если IFooService уже добавлен при запуске, он будет удален и заменен. А если это не так, он будет добавлен.

Другие вопросы по тегам