Как unit test базовое приложение asp.net с впрыском зависимостей конструктора
У меня есть основное приложение asp.net, которое использует инъекцию зависимостей, определенную в классе startup.cs приложения:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration["Data:FotballConnection:DefaultConnection"]));
// Repositories
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserRoleRepository, UserRoleRepository>();
services.AddScoped<IRoleRepository, RoleRepository>();
services.AddScoped<ILoggingRepository, LoggingRepository>();
// Services
services.AddScoped<IMembershipService, MembershipService>();
services.AddScoped<IEncryptionService, EncryptionService>();
// new repos
services.AddScoped<IMatchService, MatchService>();
services.AddScoped<IMatchRepository, MatchRepository>();
services.AddScoped<IMatchBetRepository, MatchBetRepository>();
services.AddScoped<ITeamRepository, TeamRepository>();
services.AddScoped<IFootballAPI, FootballAPIService>();
Это позволяет что-то вроде этого:
[Route("api/[controller]")]
public class MatchController : AuthorizedController
{
private readonly IMatchService _matchService;
private readonly IMatchRepository _matchRepository;
private readonly IMatchBetRepository _matchBetRepository;
private readonly IUserRepository _userRepository;
private readonly ILoggingRepository _loggingRepository;
public MatchController(IMatchService matchService, IMatchRepository matchRepository, IMatchBetRepository matchBetRepository, ILoggingRepository loggingRepository, IUserRepository userRepository)
{
_matchService = matchService;
_matchRepository = matchRepository;
_matchBetRepository = matchBetRepository;
_userRepository = userRepository;
_loggingRepository = loggingRepository;
}
Это очень аккуратно. Но становится проблемой, когда я хочу unit test. Поскольку моя тестовая библиотека не имеет startup.cs, где я устанавливаю инъекцию зависимостей. Таким образом, класс с этими интерфейсами как params будет просто нулевым.
namespace TestLibrary
{
public class FootballAPIService
{
private readonly IMatchRepository _matchRepository;
private readonly ITeamRepository _teamRepository;
public FootballAPIService(IMatchRepository matchRepository, ITeamRepository teamRepository)
{
_matchRepository = matchRepository;
_teamRepository = teamRepository;
В приведенном выше коде в тестовой библиотеке _matchRepository и _teamRepository будет просто null.: (
Могу ли я сделать что-то вроде ConfigureServices, где я определяю инъекцию зависимостей в проекте моей тестовой библиотеки?
Ответы
Ответ 1
У ваших контроллеров в ядре .net с самого начала есть инъекция зависимостей, но это не означает, что вам необходимо использовать контейнер инъекции зависимостей.
Для более простого класса:
public class MyController : Controller
{
private readonly IMyInterface _myInterface;
public MyController(IMyInterface myInterface)
{
_myInterface = myInterface;
}
public JsonResult Get()
{
return Json(_myInterface.Get());
}
}
public interface IMyInterface
{
IEnumerable<MyObject> Get();
}
public class MyClass : IMyInterface
{
public IEnumerable<MyObject> Get()
{
// implementation
}
}
Итак, в вашем приложении вы используете контейнер инъекций зависимостей в своем startup.cs
, который не более чем обеспечивает конкрецию MyClass
для использования, когда встречается IMyInterface
. Это не означает, что это единственный способ получить экземпляры MyController
.
В сценарии тестирования unit вы можете (и должны) предоставить свою собственную реализацию (или mock/stub/fake) IMyInterface
следующим образом:
public class MyTestClass : IMyInterface
{
public IEnumerable<MyObject> Get()
{
List<MyObject> list = new List<MyObject>();
// populate list
return list;
}
}
и в вашем тесте:
[TestClass]
public class MyControllerTests
{
MyController _systemUnderTest;
IMyInterface _myInterface;
[TestInitialize]
public void Setup()
{
_myInterface = new MyTestClass();
_systemUnderTest = new MyController(_myInterface);
}
}
Итак, для области модульного тестирования MyController
фактическая реализация IMyInterface
не имеет значения (и не должна), имеет значение только сам интерфейс. Мы предоставили "поддельную" реализацию IMyInterface
через MyTestClass
, но вы также можете сделать это с помощью mock, как через Moq
или RhinoMocks
.
В нижней строке вам фактически не нужен контейнер инъекции зависимостей для выполнения ваших тестов, а только отдельный, контролируемый, реализация /mock/stub/fake из ваших проверенных зависимостей классов.
Ответ 2
Вы можете проверить MyTested.AspNetCore.Mvc, что помогает с этой проблемой впрыска конструктора.
Вы создаете TestStartup class
в своем тестовом проекте, наследуя от исходного:
using MyTested.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
public class TestStartup : Startup
{
public void ConfigureTestServices(IServiceCollection services)
{
base.ConfigureServices(services);
services.Replace<IService, IMockedService>();
}
}
Обратите внимание, что вам не нужно изменять конфигурацию SQL Server EntityFramework - библиотека заменит это на базу данных с областью памяти, которая после каждого теста будет reset.
Тогда ваш unit test прост, библиотека будет вводить службы в контроллер:
[Fact]
public void ReturnViewWhenCallingIndexAction()
{
MyMvc
.Controller<MatchController>()
.WithDbContext(db => db.WithEntities(entities => entities
.AddRange(SampleDataProvider.GetMatches())))
.Calling(c => c.Get(1))
.ShouldReturn()
.Ok()
.WithResponseModelOfType<ResponseModel>()
.Passing(m =>
{
Assert.AreEqual(1, m.Id);
Assert.AreEqual("Some property value", m.SomeProperty);
});
}
Для получения дополнительной информации проверьте samples и веб-сайт.
Ответ 3
Зачем вам нужно вводить те из тестового класса?
Обычно вы тестируете MatchController, например, используя инструмент RhinoMocks для создания заглушек или макетов. Вот пример использования этого и MSTest, из которого вы можете экстраполировать:
[TestClass]
public class MatchControllerTests
{
private readonly MatchController _sut;
private readonly IMatchService _matchService;
public MatchControllerTests()
{
_matchService = MockRepository.GenerateMock<IMatchService>();
_sut = new ProductController(_matchService);
}
[TestMethod]
public void DoSomething_WithCertainParameters_ShouldDoSomething()
{
_matchService
.Expect(x => x.GetMatches(Arg<string>.Is.Anything))
.Return(new []{new Match()});
_sut.DoSomething();
_matchService.AssertWasCalled(x => x.GetMatches(Arg<string>.Is.Anything);
}
Ответ 4
Хотя ответ @Kritner правильный, я предпочитаю следующее для целостности кода и лучшего опыта DI:
[TestClass]
public class MatchRepositoryTests
{
private readonly IMatchRepository matchRepository;
public MatchRepositoryTests()
{
var services = new ServiceCollection();
services.AddTransient<IMatchRepository, MatchRepository>();
var serviceProvider = services.BuildServiceProvider();
matchRepository = serviceProvider.GetService<IMatchRepository>();
}
}