簡體   English   中英

如何在 .NET Core 中對 Startup.cs 進行單元測試

[英]How to Unit Test Startup.cs in .NET Core

人們 go 如何在 .NET Core 2 應用程序中對他們的 Startup.cs 類進行單元測試? 所有功能似乎都由 Static 擴展方法提供,這些方法不可模擬?

如果以這個ConfigureServices方法為例:

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

    services.AddMvc();
}

我如何編寫測試以確保AddDbContext(...) & AddMvc() - 通過擴展方法實現所有這些功能的選擇似乎使其無法測試?

是的,如果你想檢查在你遇到麻煩的services調用擴展方法AddDbContext的事實。 好的是你不應該真正檢查這個事實。

Startup類是應用程序組合根 在測試組合根時,您要檢查它是否實際注冊了根對象實例化所需的所有依賴項(在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);
}

作為@datchung 對 ASP.net Core 6(或 7)最小啟動的回答的替代方法,可以利用WebApplicationFactory<T>來運行啟動。 請注意,這需要從 API 定義 InternalsVisibleTo 以測試項目的Program引用是否可訪問。

示例測試,使用 xUnit:

[Fact]
public void StartupTest()
{
    var waf = new WebApplicationFactory<Program>();
    var server = waf.Server;
    // Optional: check for individual services
    var myService = server.Services.GetService<IMyService>();
    Assert.NotNull(myService);
}

那里的.Server調用會觸發測試服務器和 ServiceCollection 構建。 反過來,除非“ValidateOnBuild”選項已關閉,否則會觸發驗證

更多關於 WAF 內部的信息: https://andrewlock.net/exploring-do.net-6-part-6-supporting-integration-tests-with-webapplicationfactory-in-do.net-6/

所有這些都需要您的啟動代碼在測試場景中工作(它不應該連接到在線服務等),但這對於集成測試也很有用(例如 Alba)。

這種方法有效,並使用真正的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
}

我也遇到了類似的問題,但是設法通過在AspNetCore中使用WebHost來解決這個問題,並且基本上重新創建了program.cs所做的事情,然后斷言我的所有服務都存在並且不是null。 您可以更進一步,使用.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) { }
}

就我而言,我使用的是 .NET 6 和最小的 API(沒有 Startup 類)。

我的 Program.cs 原來是這樣的:

// using statements
...
var builder = WebApplication.CreateBuilder(args);
...
builder.services.AddSingleton<IMyInterface, MyImplementation>();
...

我添加了 StartupHelper.cs:

public class StartupHelper
{
    private readonly IServiceCollection _services;

    public StartupHelper(IServiceCollection services)
    {
        _services = services;
    }

    public void SetUpServices()
    {
        _services.AddSingleton<IMyInterface, MyImplementation>();
    }
}

我在 Program.cs 中使用了 StartupHelper:

// using statements
...
var builder = WebApplication.CreateBuilder(args);
...
var startupHelper = new StartupHelper(builder.Services);
startupHelper.SetUpServices();
...

我的測試(NUnit)看起來像這樣:

[Test]
public void SetUpServices()
{
    var builder = WebApplication.CreateBuilder(new string[0]);
    var startupHelper = new StartupHelper(builder.Services);

    startupHelper.SetUpServices();

    var app = builder.Build();

    var myImplementation = app.Services.GetService<IMyInterface>();
    Assert.NotNull(myImplementation);
    Assert.IsTrue(myImplementation is MyImplementation);
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM