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

Rob*_*lam 27 c# unit-testing mocking asp.net-core

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

如果您采用此ConfigureServices方法为例:

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

    services.AddMvc();
}
Run Code Online (Sandbox Code Playgroud)

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

Cod*_*ler 28

是的,如果你想检查扩展方法AddDbContext被调用的事实,services你就遇到了麻烦.好的是你不应该真正检查这个事实.

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

假设您有以下控制器:

public class TestController : Controller
{
    public TestController(ISomeDependency dependency)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以尝试检查是否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);
}
Run Code Online (Sandbox Code Playgroud)

  • 感谢CodeFuller,它是我丢失的ServiceCollection的具体版本,我可以让集合构建然后验证输出以确认已经进行了正确的调用.好一个! (2认同)
  • 抱歉,但我一点也不喜欢这个 - 你为什么要嘲笑 ConfigurationSection?而不是使用内置的?然后使用 InMemory 提供程序来获取部分? (2认同)

War*_*avy 11

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

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

[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) { }
}
Run Code Online (Sandbox Code Playgroud)

  • 无法完成这项工作,因为我有“IHost”。最终结果是: `var host = Program.CreateHostBuilder&lt;Startup&gt;(Array.Empty&lt;string&gt;()).Build();` 向 Program 添加了一个 Type 参数,因此在测试中我可以将其交换出来。 (2认同)