Как 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>();
    }
}