як модульно протестувати основний додаток asp.net із введенням залежностей конструктора -


81

У мене є основна програма 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;
    }

Це дуже акуратно. Але стає проблемою, коли я хочу юніт-тест. Оскільки у моїй тестовій бібліотеці немає 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, де я визначаю введення залежностей у своєму тестовому проекті бібліотеки?


2
В рамках тесту ви повинні встановити залежності для вашої системи, що перевіряється (SUT). Зазвичай ви робите це, створюючи макет залежностей перед створенням SUT. Але створити SUT, просто зателефонувавши new SUT(mockDependency);, добре для вашого тесту.
Стівен Росс

Відповіді:


33

Ваші контролери в ядрі .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 .

У сценарії модульного тестування ви можете (і повинні) надати власну реалізацію (або макет / заглушку / підробку) 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, але ви також можете зробити це з макетом, як через MoqабоRhinoMocks .

Підсумок: для виконання тестів вам насправді не потрібен контейнер для введення залежностей, а лише окрема, керована, реалізація / макет / заглушка / підробка залежностей ваших тестованих класів.


1
Ідеальна відповідь. Я навіть пішов би так далеко, що взагалі не використовував DI-контейнер у своєму модульному тесті. Відступ, за винятком одиничних тестів, спрямованих на перевірку правильності конфігурації DI, як, наприклад, порядок застосовуваних декораторів
Ric .Net

38
Я не впевнений, наскільки це корисно, коли у вас є класи за класами, котрі всі потребують ряду залежностей для введення. Що я хотів би зробити, це мати можливість реєструвати реалізації за замовчуванням (або висміювати поведінку за замовчуванням), щоб я міг створити екземпляр цих графіків об’єктів без необхідності спочатку встановлювати 30 залежностей, а скоріше переконфігурувати ті, які мені потрібні для тесту.
Sinaesthetic

1
@Sinaesthetic - це те, для чого призначені рамки тестування та глузування. nUnit дозволяє вам створювати одноразові методи або методи запуску за тестом, які дозволяють знущатися над усім, тоді у ваших тестах слід займатися лише налаштуванням методу, який ви тестуєте. Насправді використання DI для тесту означає, що це вже не Unit-Test, а інтеграційний тест з Microsofts (або 3rd Partys) DI.
Ерік Філіпс,

2
"Насправді використання DI для тесту означає, що це вже не Unit-Test", насправді не можу з вами погодитись, принаймні не за номіналом. Часто DI необхідно просто ініціалізувати клас, щоб пристрій можна було перевірити. Справа в тому, що залежності знущаються, щоб ви могли перевірити поведінку блоку навколо залежності. Я думаю, ви можете мати на увазі сценарій, коли можна було б ін'єктувати повністю функціональну залежність, тоді це може бути тест інтеграції, якщо тільки залежності цього об'єкта також не знущаються. Є багато разових сценаріїв, які можна обговорити.
Сінестетичний

У наших модульних тестах ми активно використовуємо ін’єкцію залежностей. Ми широко використовуємо їх для глузування. Я не впевнений, чому взагалі ви не хотіли б використовувати DI у своїх тестах. Ми не розробляємо програмне забезпечення нашої інфраструктури для тестів, а скоріше використовуємо DI, щоб полегшити макетування та введення об’єктів, які потрібні для тесту. І ми можемо точно налаштувати, які об’єкти доступні у нас, ServiceCollectionз самого початку. Це особливо корисно для будівельних лісів, а також для інтеграційних тестів, так що ... так, я б ЗА ВИКОРИСТАННЯ DI у ваших тестах.
Пол Карлтон,

128

Хоча відповідь @ Kritner є правильною, я віддаю перевагу наступному для цілісності коду та кращого досвіду DI:

[TestClass]
public class MatchRepositoryTests
{
    private readonly IMatchRepository matchRepository;

    public MatchRepositoryTests()
    {
        var services = new ServiceCollection();
        services.AddTransient<IMatchRepository, MatchRepositoryStub>();

        var serviceProvider = services.BuildServiceProvider();

        matchRepository = serviceProvider.GetService<IMatchRepository>();
    }
}

як ви отримали загальний метод GetService <>?
Chazt3n

Вибачте, я не міг зрозуміти, що саме ви маєте на увазі. Будь ласка, надайте більше деталей? @ Chazt3n
madjack

6
GetService<>є деякі перевантаження, які можна виявитиusing Microsoft.Extensions.DependencyInjection
Невіл Назеран,

10
Я щойно перевірив це. це набагато правильніша відповідь, ніж позначена відповідь. Для цього використовується DI. Я спробував використовувати це через ту саму функцію розширення, яку я використовую для веб-сайту. ця функція працює ідеально
Невіл Назеран,

9
Це не тестує модуль служби, це інтеграційний тест з Microsofts DI. Microsoft вже має модульні тести для тестування DI, тому немає причин робити це. Якщо ви хочете протестувати це, і об'єкт зареєстровано, це відокремлення проблем і повинно бути у його власному тесті. Unit-Testing і object означає тестування самого об'єкта без зовнішніх залежностей.
Ерік Філіпс,

38

Простим способом я написав загальний допоміжний клас вирішувача залежностей, а потім створив IWebHost у своєму модульному тестовому класі.

Загальний вирішувач залежностей

    public class DependencyResolverHelpercs
    {
        private readonly IWebHost _webHost;

        /// <inheritdoc />
        public DependencyResolverHelpercs(IWebHost WebHost) => _webHost = WebHost;

        public T GetService<T>()
        {
            using (var serviceScope = _webHost.Services.CreateScope())
            {
                var services = serviceScope.ServiceProvider;
                try
                {
                    var scopedService = services.GetRequiredService<T>();
                    return scopedService;
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                    throw;
                }
            };
        }
    }
}

Проект модульних випробувань

  [TestFixture]
    public class DependencyResolverTests
    {
        private DependencyResolverHelpercs _serviceProvider;

        public DependencyResolverTests()
        {

            var webHost = WebHost.CreateDefaultBuilder()
                .UseStartup<Startup>()
                .Build();
            _serviceProvider = new DependencyResolverHelpercs(webHost);
        }

        [Test]
        public void Service_Should_Get_Resolved()
        {

            //Act
            var YourService = _serviceProvider.GetService<IYourService>();

            //Assert
            Assert.IsNotNull(YourService);
        }


    }

гарний приклад про те, як замінити Autofac на Microsoft.Extensions.DependencyInjection
lnaie

Так, це має бути відповідь за замовчуванням. Ми розмістили цілий набір, який тестує всі послуги, які потрібно вводити, і це працює як шарм. Дякую!
facundofarias

привіт @ Джошуа Даксбері, ти можеш допомогти відповісти на це питання? stackoverflow.com/questions/57331395/… намагаючись реалізувати своє рішення, щойно надіслав 100 балів, переглядаючи й інші ваші відповіді, дякую!

3
Розпорядження сферою мені здається неправильним - docs.microsoft.com/en-us/dotnet/api/… - Once Dispose is called, any scoped services that have been resolved from ServiceProvider will be disposed..
Євген Подскал,

1
Можливо, вам доведеться видалити оператор "using", щоб уникнути "помилки утилізованого об'єкта" у ваших DBcontexts
Моста,

4

Якщо ви використовуєте конвенцію Program.cs+ Startup.csі хочете швидко запустити цю роботу, ви можете повторно використати свій існуючий конструктор хостів за допомогою одного рядка:

using MyWebProjectNamespace;

public class MyTests
{
    readonly IServiceProvider _services = 
        Program.CreateHostBuilder(new string[] { }).Build().Services; // one liner

    [Test]
    public void GetMyTest()
    {
        var myService = _services.GetRequiredService<IMyService>();
        Assert.IsNotNull(myService);
    }
}

Зразок Program.csфайлу з веб-проекту:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace MyWebProjectNamespace
{
    public class Program
    {
        public static void Main(string[] args) =>
            CreateHostBuilder(args).Build().Run();

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Казково! Дуже дякую. Я бачу, як налаштування нового ServiceCollection у [SetUp]може бути корисним або навіть знущанням над залежностями. Але насправді я хочу зробити те, що використовую ту саму колекцію служб, яка використовується моєю веб-програмою, і запускаю тести проти того самого середовища. На здоров’я!
Nexus

0

Чому ви хочете вводити ті, хто навчається у тестовому класі? Зазвичай ви тестуєте 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);
    }

Пакет RhinoMocks 3.6.1 не сумісний з netcoreapp1.0 (.NETCoreApp, Версія = v1.0). Пакет RhinoMocks 3.6.1 підтримує: net (.NETFramework, Версія = v0.0)
ganjan

Інші рамки повільно приймають цей набір.
Александру Маркулеску
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.