Как Unit Test Startup.cs в .NET Core

Как люди идут на модульное тестирование своих классов Startup.cs в приложении .NET Core 2? Все функции, как представляется, предоставляются методами статических расширений, которые не являются имитируемыми?

Если вы берете этот метод ConfigureServices, например:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<BlogContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddMvc();
}

Как я могу написать тесты, чтобы гарантировать, что вызывается AddDbContext (...) и AddMvc(), выбор реализации всей этой функции с помощью методов расширений, кажется, сделал ее неэлектируемой?

Ответ 1

Ну да, если вы хотите проверить факт, что метод расширения AddDbContext был вызван на services, у вас проблемы. Хорошо, что вы действительно не должны точно проверять этот факт.

Startup class - это приложение составной корень. И при тестировании корня композиции вы хотите проверить, что он фактически регистрирует все зависимости, необходимые для создания экземпляров корневых объектов (контроллеры в случае приложения ASP.NET Core).

Скажем, у вас есть контроллер:

public class TestController : Controller
{
    public TestController(ISomeDependency dependency)
    {
    }
}

Вы можете попробовать проверить, зарегистрировал ли Startup тип для ISomeDependency. Но реализация ISomeDependency также может потребовать некоторых других зависимостей, которые вы должны проверить. В конце концов вы закончите тест, который содержит множество проверок для разных зависимостей, но на самом деле не гарантирует, что разрешение объекта не будет вызывать отсутствующее исключение зависимостей. В таком тесте не так уж много значения.

Подход, который хорошо работает для меня при тестировании корня композиции, заключается в использовании контейнера для инъекций реальной зависимости. Затем я вызываю на него корневой состав и утверждаю, что разрешение корневого объекта не выбрасывается.

Он не может считаться чистым Unit Test, потому что мы используем другой не-заглубленный класс. Но такие тесты, в отличие от других интеграционных тестов, являются быстрыми и стабильными. И самое главное, они приносят значение действительной проверки для правильной регистрации зависимостей. Если такие проверки пройдут, вы можете быть уверены, что объект также будет правильно создан в продукте.

Вот пример такого теста:

[TestMethod]
public void ConfigureServices_RegistersDependenciesCorrectly()
{
    //  Arrange

    //  Setting up the stuff required for Configuration.GetConnectionString("DefaultConnection")
    Mock<IConfigurationSection> configurationSectionStub = new Mock<IConfigurationSection>();
    configurationSectionStub.Setup(x => x["DefaultConnection"]).Returns("TestConnectionString");
    Mock<Microsoft.Extensions.Configuration.IConfiguration> configurationStub = new Mock<Microsoft.Extensions.Configuration.IConfiguration>();
    configurationStub.Setup(x => x.GetSection("ConnectionStrings")).Returns(configurationSectionStub.Object);

    IServiceCollection services = new ServiceCollection();
    var target = new Startup(configurationStub.Object);

    //  Act

    target.ConfigureServices(services);
    //  Mimic internal asp.net core logic.
    services.AddTransient<TestController>();

    //  Assert

    var serviceProvider = services.BuildServiceProvider();

    var controller = serviceProvider.GetService<TestController>();
    Assert.IsNotNull(controller);
}

Ответ 2

Этот подход работает и использует настоящий конвейер MVC, так как вещи должны быть смоделированы, только если вам нужно изменить, как они работают.

public void AddTransactionLoggingCreatesConnection()
{
     var servCollection = new ServiceCollection();

    //Add any injection stuff you need here
    //servCollection.AddSingleton(logger.Object);

    //Setup the MVC builder thats needed
    IMvcBuilder mvcBuilder = new MvcBuilder(servCollection, new Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager());

    IEnumerable<KeyValuePair<string, string>> confValues = new List<KeyValuePair<string, string>>()
    {
        new KeyValuePair<string, string>("TransactionLogging:Enabled", "True"),
        new KeyValuePair<string, string>("TransactionLogging:Uri", "https://api.something.com/"),
        new KeyValuePair<string, string>("TransactionLogging:Version", "1"),
        new KeyValuePair<string, string>("TransactionLogging:Queue:Enabled", "True")
    };

    ConfigurationBuilder builder = new ConfigurationBuilder();
    builder.AddInMemoryCollection(confValues);

    var confRoot = builder.Build();
    StartupExtensions.YourExtensionMethod(mvcBuilder); // Any other params
}

Ответ 3

У меня также была похожая проблема, но мне удалось обойти ее, используя WebHost в AspNetCore и по существу заново создавая то, что делает program.cs, а затем утверждая, что все мои сервисы существуют и не являются нулевыми. Вы можете пойти еще дальше и выполнить определенные расширения для IServices с помощью .ConfigureServices или фактически выполнить операции со службами, которые вы создали, чтобы убедиться, что они были построены правильно.

Одним из ключевых моментов является то, что я создал класс запуска модульного теста, который наследуется от класса запуска, который я тестирую, так что мне не нужно беспокоиться об отдельных сборках. Вы можете использовать композицию, если вы предпочитаете не использовать наследование.

[TestClass]
public class StartupTests
{
    [TestMethod]
    public void StartupTest()
    {
        var webHost = Microsoft.AspNetCore.WebHost.CreateDefaultBuilder().UseStartup<Startup>().Build();
        Assert.IsNotNull(webHost);
        Assert.IsNotNull(webHost.Services.GetRequiredService<IService1>());
        Assert.IsNotNull(webHost.Services.GetRequiredService<IService2>());
    }
}

public class Startup : MyStartup
{
    public Startup(IConfiguration config) : base(config) { }
}