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