如何在.NET Core中对Startup.cs进行单元测试

时间:2017-11-25 03:05:35

标签: c# unit-testing asp.net-core mocking

人们如何在.NET Core 2应用程序中测试他们的Startup.cs类?所有功能似乎都是由静态扩展方法提供的,这些方法不可模拟?

如果你采用这个ConfigureServices方法:

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

    services.AddMvc();
}

如何编写测试以确保AddDbContext(...)&amp;调用AddMvc(),通过Extensions方法实现所有这些功能的选择似乎使它不可测试?

3 个答案:

答案 0 :(得分:21)

是的,如果你想检查在AddDbContext上调用扩展方法services的事实,你就遇到了麻烦。 好的是你不应该确切地检查这个事实。

Startup类是一个应用程序composition root。在测试组合根时,您要检查它是否实际注册了根对象实例化所需的所有依赖项(在ASP.NET Core应用程序的情况下为控制器)。

假设您有以下控制器:

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

您可以尝试检查Startup是否已注册ISomeDependency的类型。但是ISomeDependency的实现也可能需要您应该检查的其他一些依赖项。 最终,您最终会得到一个针对不同依赖项进行大量检查的测试,但它实际上并不能保证对象解析不会丢失缺少的依赖项异常。在这样的测试中没有太多的价值。

在测试组合根时,一种适合我的方法是使用真正的依赖注入容器。然后我在其上调用组合根并断言根对象的解析不会抛出。

它不能被视为纯单元测试,因为我们使用其他非存根类。但是,与其他集成测试不同,此类测试快速而稳定。最重要的是,它们为正确的依赖注册带来了有效检查的价值。如果此类测试通过,您可以确定该对象也将在产品中正确实例化。

以下是此类测试的示例:

[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);
}

答案 1 :(得分:0)

这种方法有效,并且使用了真正的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);
    }
}

答案 2 :(得分:0)

我也遇到了类似的问题,但是设法通过使用AspNetCore中的WebHost并基本上重新创建了program.cs所做的事情来解决该问题,然后断言我的所有服务都存在并且不为空。您可以更进一步,并使用.ConfigureServices对IServices执行特定的扩展,或者对创建的服务执行实际操作以确保它们的构造正确。

一个关键就是我创建了一个单元测试启动类,该类继承自我正在测试的启动类,这样我就不必担心单独的程序集。如果您不想使用继承,则可以使用组合。

<BrowserRouter>
    <div className='App'>
        <MainHeader />
        <Route exact path='/' render={props => (
            <React.Fragment>
                <Homepage/>
                <Placeholder/>
            </React.Fragment>
        )}/>
        <Route path='/placeholder' component={Placeholder} />
        <Route path='/address_checker' component={Address_Checker} />
        <Route path='/availability_checker' component={Availability_Checker} />
        <Route path='/usage_checker' component={Usage_Checker} />
        <Route path='/device_checker' component={Device_Checker} />
        <Route path='/payment_checker' component={Payment_Checker} />
        <Route path='/result' component={Result} />

        <Route path='/login' component={Login} />
    </div>
</BrowserRouter>