Как я могу отключить/издеваться над сервисами в тестах ASP.NET Core?

Я бы хотел протестировать некоторые классы, которые являются частью проекта ASP.NET Web Api. Мне не нужны тесты интеграции запросов и ответов через TestServer (хотя они приятные), но я хочу, чтобы мои тесты были как можно ближе к "реальной вещи". Поэтому я хочу разрешить свои классы с помощью сервисов, добавленных в Startup, но меняя некоторые из них на заглушки/макеты на тестовой основе (некоторые тесты нуждаются в mocks, другие - нет).

Это было очень легко сделать в старые добрые времена, когда ASP.NET не имел внутренней среды инъекций инъекций. Поэтому я бы просто вызвал класс, который регистрирует все зависимости для контейнера, а затем создает дочерний контейнер для каждого теста, меняет некоторые зависимости на издевку и что он.

Я попробовал что-то вроде этого:

    var host = A.Fake<IHostingEnvironment>();
    var startup = new Startup(host);
    var services = new ServiceCollection();
    //Add stubs here
    startup.ConfigureServices(services);
    var provider = services.BuildServiceProvider();
    provider.GetService<IClientsHandler>();

Кажется, что это работает, но я не хочу создавать всю инфраструктуру запуска для каждого теста. Я хотел бы создать его один раз, а затем создать "дочерний контейнер" или "область содержимого" для каждого теста. Является ли это возможным? В основном я ищу способ изменить услуги за пределами Startup.

Ответ 1

Создание дочерних областей для каждого запроса может быть выполнено при настройке HttpConfiguration путем создания пользовательского IHttpControllerActivator.

Это для OWIN, но преобразование в.Net Core должно быть довольно простым: https://gist.github.com/jt000/eef096a2341471856e8a86d06aaec887

Важные части предназначены для создания Scope & Controller в этой области...

var scope = _provider.CreateScope();
request.RegisterForDispose(scope);

var controller = scope.ServiceProvider.GetService(controllerType) as IHttpController;

... и перезапись по умолчанию IHttpControllerActivator...

config.Services.Replace(typeof (IHttpControllerActivator), new ServiceProviderControllerActivator(parentActivator, provider));

Теперь вы можете добавить свои контроллеры, которые будут созданы с помощью IServiceProvider с включенной функцией Dependency Injection...

services.AddScoped<ValuesController>((sp) => new ValuesController(sp.GetService<ISomeCustomService>()));

Чтобы проверить свой ValuesController в рамках модульных тестов, я бы предложил использовать что-то вроде инфраструктуры Moq, чтобы издеваться над методами в ваших сервисных интерфейсах. Например:

var someCustomService = Mock.Of<ISomeCustomService>(s => s.DoSomething() == 3);
var sut = new ValuesController(someCustomService);

var result = sut.Get();

Assert.AreEqual(result, new [] { 3 });

Ответ 2

На всякий случай вы хотели бы использовать те же Web API Core и инфраструктуру DI для ваших модульных тестов xUnit (в этом случае я бы назвал их интеграционными тестами), я бы предложил переместить контекст TestServer и HttpClient в базовый класс, реализующий xUnit IClassFixture,

В этом случае вы будете тестировать API со всеми сервисами и DI настроенными в вашем реальном Web API Core:

public class TestServerDependent : IClassFixture<TestServerFixture>
{
    private readonly TestServerFixture _fixture;
    public TestServer TestServer => _fixture.Server;
    public HttpClient Client => _fixture.Client;

    public TestServerDependent(TestServerFixture fixture)
    {
        _fixture = fixture;
    }

    protected TService GetService<TService>()
        where TService : class
    {
        return _fixture.GetService<TService>();
    }
}

public class TestServerFixture : IDisposable
{
    public TestServer Server { get; }
    public HttpClient Client { get; }

    public TestServerFixture()
    {
        // UseStaticRegistration is needed to workaround AutoMapper double initialization. Remove if you don't use AutoMapper.
        ServiceCollectionExtensions.UseStaticRegistration = false;

        var hostBuilder = new WebHostBuilder()
            .UseEnvironment("Testing")
            .UseStartup<Startup>();

        Server = new TestServer(hostBuilder);
        Client = Server.CreateClient();
    }

    public void Dispose()
    {
        Server.Dispose();
        Client.Dispose();
    }

    public TService GetService<TService>()
        where TService : class
    {
        return Server?.Host?.Services?.GetService(typeof(TService)) as TService;
    }
}

Тогда вы можете просто извлечь из этого класса, чтобы протестировать действие контроллера таким образом:

public class ValueControllerTests : TestServerDependent
{
    public ValueControllerTests(TestServerFixture fixture)
        : base(fixture)
    {
    }

    [Fact]
    public void Returns_Ok_Response_When_Requested()
    {
        var responseMessage = Client.GetAsync("/api/value").Result;
        Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode);
    }
}

Также вы можете протестировать службы DI:

public class MyServiceTests : TestServerDependent
{
    public MyServiceTests(TestServerFixture fixture)
        : base(fixture)
    {
    }

    [Fact]
    public void ReturnsDataWhenServiceInjected()
    {
        var service = GetService<IMyService>();
        Assert.NotNull(service);

        var data = service.GetData();
        Assert.NotNull(data);
    }
}

Ответ 3

Все это хорошо описано в документации.

Для тестов интеграции вы используете класс TestServer и предоставляете ему класс Startup (не обязательно быть StartupIntegrationTest в режиме реального времени, также можно использовать StartupIntegrationTest или Startup with Configure{Envrionment Name here}/ConfigureServices{Envrionment Name here}.

 var server = new TestServer(new WebHostBuilder()
    .UseStartup<Startup>()
    // this would cause it to use StartupIntegrationTest class or ConfigureServicesIntegrationTest / ConfigureIntegrationTest methods (if existing)
    // rather than Startup, ConfigureServices and Configure 
    .UseEnvironment("IntegrationTest")); 

Чтобы получить доступ к поставщику услуг, выполните

var server = new TestServer(new WebHostBuilder()
    .UseStartup<Startup>()
    .UseEnvironment("IntegrationTest")); 
var controller = server.Host.Services.GetService<MyService>();

Для IServiceCollection тестов вы не должны использовать IServiceCollection/IServiceProvider вообще, просто IServiceProvider над интерфейсами и вводите их.