Как я могу отключить/издеваться над сервисами в тестах 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
над интерфейсами и вводите их.