ASP.NET Core 中的整合測試

作者: Javier Calvarro 一文Steve SmithJos van der Til

整合測試可確保應用程式的元件在包含應用程式支援基礎結構的層級正常運作,例如資料庫、檔案系統和網路。 ASP.NET Core支援使用單元測試架構搭配測試 Web 主機和記憶體內部測試伺服器進行整合測試。

本主題假設對單元測試有基本的瞭解。 如果不熟悉測試概念,請參閱 .NET Core 和 .NET Standard 中的單元測試 主題及其連結內容。

檢視或下載範例程式碼 (如何下載)

範例應用程式是 Razor Pages 應用程式,並假設對 Pages 有基本的瞭解 Razor 。 如果不熟悉 Razor Pages,請參閱下列主題:

注意

為了測試 SPA,我們建議使用 適用于 .NET 的 Playwright之類的工具,可將瀏覽器自動化。

整合測試簡介

整合測試會以比 單元測試更廣泛的層級來評估應用程式的元件。 單元測試可用來測試隔離的軟體元件,例如個別類別方法。 整合測試會確認兩個以上的應用程式元件一起運作以產生預期的結果,可能包括完整處理要求所需的每個元件。

這些更廣泛的測試可用來測試應用程式的基礎結構和整個架構,通常包括下列元件:

  • 資料庫
  • 檔案系統
  • 網路設備
  • 要求-回應管線

單元測試會使用偽裝元件,稱為 模擬物件,取代基礎結構元件。

與單元測試相反,整合測試:

  • 使用應用程式在生產環境中使用的實際元件。
  • 需要更多程式碼和資料處理。
  • 需要較長的時間才能執行。

因此,請將整合測試的使用限制為最重要的基礎結構案例。 如果可以使用單元測試或整合測試來測試行為,請選擇單元測試。

在整合測試的討論中,測試的專案通常稱為 「受測系統」,或簡稱「SUT」。 本主題中會使用 「SUT」 來參考測試 ASP.NET Core應用程式。

提示

請勿針對資料庫和檔案系統的資料和檔案存取的每個可能排列撰寫整合測試。 無論應用程式與資料庫和檔案系統之間有多少個位置互動,一組專注的讀取、寫入、更新和刪除整合測試通常能夠適當地測試資料庫和檔案系統元件。 針對與這些元件互動之方法邏輯的例行測試使用單元測試。 在單元測試中,使用基礎結構 fakes/mock 會導致更快速的測試執行。

ASP.NET Core整合測試

ASP.NET Core中的整合測試需要下列各項:

  • 測試專案可用來包含和執行測試。 測試專案具有 SUT 的參考。
  • 測試專案會建立 SUT 的測試 Web 主機,並使用測試伺服器用戶端來處理與 SUT 的要求和回應。
  • 測試執行器可用來執行測試和報告測試結果。

整合測試會遵循包含一般 ArrangeActAssert 測試步驟的事件序列:

  1. 已設定 SUT 的 Web 主機。
  2. 系統會建立測試伺服器用戶端,以將要求提交至應用程式。
  3. 執行 Arrange 測試步驟:測試應用程式會準備要求。
  4. 執行 Act測試步驟:用戶端會提交要求並接收回應。
  5. 執行 Assert測試步驟:根據預期的回應,實際回應會驗證為通過失敗
  6. 此程式會繼續執行,直到執行所有測試為止。
  7. 系統會報告測試結果。

通常,測試 Web 主機的設定方式與測試回合的應用程式一般 Web 主機不同。 例如,不同的資料庫或不同的應用程式設定可能會用於測試。

Microsoft.AspNetCore.Mvc.Testing套件提供或管理基礎結構元件,例如測試 Web 主機和記憶體內部測試伺服器 TestServer () 。 使用此套件可簡化測試建立和執行。

封裝 Microsoft.AspNetCore.Mvc.Testing 會處理下列工作:

  • 將相依性檔案 (.deps) 從 SUT 複製到測試專案的 bin 目錄中。
  • 內容根 目錄設定為 SUT 的專案根目錄,以便在執行測試時找到靜態檔案和頁面/檢視。
  • 提供 WebApplicationFactory 類別,以使用 TestServer 簡化啟動載入 SUT。

單元測試檔說明如何設定測試專案和測試執行器,以及有關如何命名測試和測試類別的詳細指示。

注意

為應用程式建立測試專案時,請將單元測試與整合測試分隔成不同的專案。 這有助於確保基礎結構測試元件不會意外包含在單元測試中。 區隔單元和整合測試也可讓您控制要執行哪一組測試。

頁面應用程式和 MVC 應用程式測試的 Razor 設定幾乎沒有任何差異。 唯一的差異在於如何命名測試。 Razor在 Pages 應用程式中,頁面端點的測試通常會以頁面模型類別命名 (例如, IndexPageTests 以測試索引頁面) 的元件整合。 在 MVC 應用程式中,測試通常會依控制器類別組織,並以控制器類別命名,例如測試 (控制器, HomeControllerTests 以測試控制器) 的 Home 元件整合。

測試應用程式必要條件

測試專案必須:

您可以在 範例應用程式中查看這些必要條件。 tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj檢查檔案。 範例應用程式會使用 xUnit 測試架構和 AngleSharp 剖析器程式庫,因此範例應用程式也會參考:

在使用 2.4.2 版或更新版本的應用程式中 xunit.runner.visualstudio ,測試專案必須參考 Microsoft.NET.Test.Sdk 套件。

Entity Framework Core 也會用於測試中。 應用程式參考:

SUT 環境

如果未設定 SUT 的環境 ,環境預設為 [開發]。

預設 WebApplicationFactory 的基本測試

ASP.NET Core 6 引進 WebApplication ,其已移除類別 Startup 的需求。 若要在沒有 WebApplicationFactory 類別的情況下 Startup 進行測試,ASP.NET Core 6 應用程式必須執行下列其中一項,將隱含定義的 Program 類別公開至測試專案:

  • 將 Web 應用程式的內部類型公開至測試專案。 這可以在專案檔 () .csproj 完成:
    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Program使用部分類別宣告將類別設為公用:
    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

在 Web 應用程式中進行變更之後,測試專案現在可以使用 的 ProgramWebApplicationFactory 類別。

[Fact]
public async Task HelloWorldTest()
{
    var application = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            // ... Configure test services
        });

    var client = application.CreateClient();
    //...
}

WebApplicationFactory<TEntryPoint> 用來建立 TestServer 整合測試的 。 TEntryPoint 是 SUT 的進入點類別,通常是 Startup 類別。

測試類別會實作 類別裝置 介面 (IClassFixture) ,指出類別包含測試,並在 類別的測試之間提供共用物件實例。

下列測試類別 BasicTests 會使用 WebApplicationFactory 來啟動 SUT,並將 提供給 HttpClient 測試方法 Get_EndpointsReturnSuccessAndCorrectContentType 。 方法會檢查回應狀態碼是否成功 (200-299) 中的狀態碼,而 Content-Type 標頭適用于 text/html; charset=utf-8 數個應用程式頁面。

CreateClient() 會建立 的 HttpClient 實例,這個實例會自動遵循重新導向和控制碼 cookie 。

public class BasicTests 
    : IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
    private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

    public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

根據預設,啟用GDPR 同意原則時,不會跨要求保留非必要的 cookie 。 若要保留非必要的 cookie ,例如 TempData 提供者所使用的專案,請在測試中將它們標示為必要專案。 如需將 標示 cookie 為必要專案的指示,請參閱 Essential cookie s

自訂 WebApplicationFactory

您可以繼承自 WebApplicationFactory 來建立一或多個自訂處理站,以獨立建立 Web 主機組態,

  1. 繼承自 WebApplicationFactory 並覆寫 ConfigureWebHostIWebHostBuilder允許使用 ConfigureServices 設定服務集合:

    public class CustomWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(descriptor);
    
                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });
    
                var sp = services.BuildServiceProvider();
    
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
    
                    db.Database.EnsureCreated();
    
                    try
                    {
                        Utilities.InitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding the " +
                            "database with test messages. Error: {Message}", ex.Message);
                    }
                }
            });
        }
    }
    

    範例應用程式中的資料庫植入是由 InitializeDbForTests 方法執行。 方法會在 整合測試範例:測試應用程式組織 一節中說明。

    SUT 的資料庫內容會在其 方法中 Startup.ConfigureServices 註冊。 執行應用程式的程式碼之後,就會執行測試應用程式的 Startup.ConfigureServicesbuilder.ConfigureServices 回呼。 執行順序是泛型主機的中斷性變更,發行為 ASP.NET Core 3.0。 若要對測試使用與應用程式資料庫不同的資料庫,必須在 中 builder.ConfigureServices 取代應用程式的資料庫內容。

    對於仍在使用Web 主機的 SUT,測試應用程式的 builder.ConfigureServices 回呼會在 SUT 的程式 Startup.ConfigureServices 代碼之前執行。 測試應用程式的 builder.ConfigureTestServices 回呼會在 之後執行。

    範例應用程式會尋找資料庫內容的服務描述項,並使用描述項來移除服務註冊。 接下來,處理站會新增新的 ApplicationDbContext ,以使用記憶體內部資料庫進行測試。

    若要連線到與記憶體內部資料庫不同的資料庫,請變更 UseInMemoryDatabase 呼叫以將內容連接到不同的資料庫。 若要使用SQL Server測試資料庫:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. 在測試類別中使用自訂 CustomWebApplicationFactory 。 下列範例使用 類別中的 IndexPageTests Factory:

    public class IndexPageTests : 
        IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> 
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
                {
                    AllowAutoRedirect = false
                });
        }
    

    範例應用程式的用戶端已設定為防止 HttpClient 下列重新導向。 如稍後的 模擬驗證 一節所述,這可讓測試檢查應用程式第一個回應的結果。 第一個回應是其中許多測試中具有 標頭的 Location 重新導向。

  3. 典型的測試會使用 HttpClient 和 協助程式方法來處理要求和回應:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

對 SUT 的任何 POST 要求都必須滿足應用程式 資料保護反Forgery 系統自動進行的反forgery檢查。 若要排列測試 POST 要求,測試應用程式必須:

  1. 提出頁面的要求。
  2. 剖析反Forgery cookie ,並從回應要求驗證權杖。
  3. 使用反Forgery cookie 提出 POST 要求,並就地要求驗證權杖。

協助 SendAsync 程式擴充方法 (Helpers/HttpClientExtensions.cs) 和 GetDocumentAsync範例應用程式中的協助程式方法 (Helpers/HtmlHelpers.cs) ,使用AngleSharp剖析器來處理反分叉檢查,方法如下:

  • GetDocumentAsync:接收 HttpResponseMessage 並傳 IHtmlDocument 回 。 GetDocumentAsync會使用根據原始 HttpResponseMessage 準備虛擬回應的處理站。 如需詳細資訊,請參閱 AngleSharp 檔
  • SendAsync撰寫 HttpRequestMessage 的擴充方法, HttpClient 並呼叫 SendAsync(HttpRequestMessage) 以將要求提交至 SUT。 接受 SendAsync HTML 表單的多載 (IHtmlFormElement) 和下列專案:
    • 表單的 [提交] 按鈕 (IHtmlElement)
    • 表單值集合 (IEnumerable<KeyValuePair<string, string>>)
    • 提交按鈕 (IHtmlElement) 和表單值 (IEnumerable<KeyValuePair<string, string>>)

注意

AngleSharp 是用於本主題和範例應用程式之示範用途的協力廠商剖析程式庫。 ASP.NET Core應用程式的整合測試不支援或需要 AngleSharp。 您可以使用其他剖析器,例如 HTML Agility Pack (HAP) 。 另一種方法是撰寫程式碼,以直接處理反forgery 系統的要求驗證權杖和反分叉 cookie 。

注意

EF-Core 記憶體內部資料庫提供者可用於有限和基本的測試,不過SQLite 提供者是記憶體內部測試的建議選項。

使用 WithWebHostBuilder 自訂用戶端

當測試方法內需要其他設定時, WithWebHostBuilder 請使用組態進一步自訂的 建立新的 WebApplicationFactoryIWebHostBuilder

範例 Post_DeleteMessageHandler_ReturnsRedirectToRoot應用程式的 測試方法示範如何使用 WithWebHostBuilder 。 此測試會藉由觸發 SUT 中的表單提交,在資料庫中執行記錄刪除。

因為 類別中的 IndexPageTests 另一個測試會執行一個作業,該作業會刪除資料庫中的所有記錄,而且可以在 方法之前 Post_DeleteMessageHandler_ReturnsRedirectToRoot 執行,因此會在此測試方法中重新設定資料庫,以確保 SUT 刪除記錄存在。 在 SUT 的要求中,會模擬 SUT 中表單的第一個刪除按鈕 messages

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                var serviceProvider = services.BuildServiceProvider();

                using (var scope = serviceProvider.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices
                        .GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<IndexPageTests>>();

                    try
                    {
                        Utilities.ReinitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding " +
                            "the database with test messages. Error: {Message}", 
                            ex.Message);
                    }
                }
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

用戶端選項

下表顯示建立 HttpClient 實例時可用的預設值 WebApplicationFactoryClientOptions

選項 描述 預設
AllowAutoRedirect 取得或設定實例是否 HttpClient 應該自動遵循重新導向回應。 true
BaseAddress 取得或設定實例的 HttpClient 基底位址。 http://localhost
HandleCookies 取得或設定實例是否 HttpClient 應該處理 cookie 。 true
MaxAutomaticRedirections 取得或設定實例應遵循的重新導向回應 HttpClient 數目上限。 7

建立 類別 WebApplicationFactoryClientOptions 並將它傳遞至 CreateClient() 方法, (預設值會顯示在程式碼範例中) :

// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;

_client = _factory.CreateClient(clientOptions);

插入模擬服務

您可以在測試中覆寫服務,並在主機產生器上呼叫 ConfigureTestServices若要插入模擬服務,SUT 必須具有 Startup 具有 方法的 Startup.ConfigureServices 類別。

範例 SUT 包含傳回引號的範圍服務。 當要求 [索引] 頁面時,引號會內嵌在 [索引] 頁面上的隱藏欄位。

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Startup.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

執行 SUT 應用程式時會產生下列標記:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

若要在整合測試中測試服務和引號插入,模擬服務會由測試插入 SUT。 模擬服務會將應用程式的 QuoteService 取代為測試應用程式所提供的服務,稱為 TestQuoteService

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices 會呼叫 ,並註冊範圍服務:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

在測試執行期間產生的標記會反映 所提供的 TestQuoteService 引號文字,因此判斷提示會通過:

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

模擬驗證

類別 AuthTests 中的測試會檢查安全端點:

  • 將未經驗證的使用者重新導向至應用程式的 [登入] 頁面。
  • 傳回已驗證使用者的內容。

在 SUT 中 /SecurePage ,頁面會使用 AuthorizePage 慣例將 套用 AuthorizeFilter 至頁面。 如需詳細資訊,請參閱Razor 頁面授權慣例

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

在測試中 Get_SecurePageRedirectsAnUnauthenticatedUser ,會將 WebApplicationFactoryClientOptions 設定為 不允許重新導向,方法是將 設定 AllowAutoRedirectfalse

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login", 
        response.Headers.Location.OriginalString);
}

不允許用戶端遵循重新導向,即可進行下列檢查:

  • SUT 傳回的狀態碼可以針對預期 HttpStatusCode.Redirect 的結果進行檢查,而不是重新導向至登入頁面之後的最終狀態代碼,也就是 HttpStatusCode.OK
  • 系統會 Location 檢查回應標頭中的標頭值,以確認其開頭為 http://localhost/Identity/Account/Login ,而不是最終的登入頁面回應,其中 Location 不會顯示標頭。

測試應用程式可以模擬 AuthenticationHandler<TOptions>ConfigureTestServices ,以測試驗證和授權的各個層面。 最小案例會傳 AuthenticateResult.Success 回 :

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

當驗證配置設定 Test 為 註冊的位置 ConfigureTestServicesAddAuthentication 時,會呼叫 來 TestAuthHandler 驗證使用者。 配置必須 Test 符合應用程式預期的配置。 否則,驗證將無法運作。

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication("Test")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "Test", options => {});
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Test");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

如需 的詳細資訊 WebApplicationFactoryClientOptions ,請參閱 用戶端選項 一節。

設定環境

根據預設,SUT 的主機和應用程式環境會設定為使用開發環境。 使用 時 IHostBuilder 覆寫 SUT 的環境:

  • ASPNETCORE_ENVIRONMENT設定環境變數 (例如 、 StagingProduction 或其他自訂值,例如 Testing) 。
  • 在測試應用程式中覆寫 CreateHostBuilder ,以讀取前面加上 的 ASPNETCORE 環境變數。
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

如果 SUT 使用 Web 主機 () IWebHostBuilder ,請覆寫 CreateWebHostBuilder

protected override IWebHostBuilder CreateWebHostBuilder() =>
    base.CreateWebHostBuilder().UseEnvironment("Testing");

測試基礎結構如何推斷應用程式內容根路徑

WebApplicationFactory 構函式會搜尋 WebApplicationFactoryContentRootAttribute 包含整合測試且索引鍵等於 TEntryPoint 元件的 System.Reflection.Assembly.FullName ,以推斷應用程式內容根路徑。 如果找不到具有正確索引鍵的屬性, WebApplicationFactory 請回到搜尋方案檔 (.sln) ,並將元件名稱附加 TEntryPoint 至方案目錄。 應用程式根目錄 (內容根路徑) 用來探索檢視和內容檔案。

停用陰影複製

陰影複製會導致測試在與輸出目錄不同的目錄中執行。 如果您的測試依賴相對於 Assembly.Location 載入檔案,而且遇到問題,您可能必須停用陰影複製。

若要在使用 xUnit 時停用陰影複製,請使用正確的組態設定,在測試專案目錄中建立 xunit.runner.json 檔案:

{
  "shadowCopy": false
}

處置物件

執行實作 IClassFixture 的測試之後, TestServer 並在 HttpClient xUnit 處置 WebApplicationFactory 時處置 。 如果開發人員具現化的物件需要處置,請在實作中 IClassFixture 處置這些物件。 如需詳細資訊,請參閱 實作 Dispose 方法

整合測試範例

範例應用程式是由兩個應用程式所組成:

應用程式 專案目錄 描述
訊息應用程式 (SUT) src/RazorPagesProject 允許使用者新增、刪除一個、刪除全部及分析訊息。
測試應用程式 tests/RazorPagesProject.Tests 用來整合測試 SUT。

您可以使用 IDE 的內建測試功能來執行測試,例如Visual Studio。 如果使用Visual Studio Code或命令列,請在目錄中的命令提示字元 tests/RazorPagesProject.Tests 執行下列命令:

dotnet test

訊息應用程式 (SUT) 組織

SUT 是 Razor 具有下列特性的 Pages 訊息系統:

  • 應用程式 (Pages/Index.cshtmlPages/Index.cshtml.cs) 的 [索引] 頁面提供 UI 和頁面模型方法來控制每個訊息) (平均單字的新增、刪除和分析。
  • 類別會以 Message 兩個屬性描述訊息 () Data/Message.csId (索引鍵) 和 Text (訊息) 。 屬性 Text 是必要的,且限制為 200 個字元。
  • 訊息會使用 Entity Framework 的記憶體內部資料庫來儲存†。
  • 應用程式在其資料庫內容類別別中包含資料存取層 (DAL) , AppDbContext (Data/AppDbContext.cs) 。
  • 如果資料庫在應用程式啟動時是空的,則會使用三則訊息初始化訊息存放區。
  • 應用程式包含 /SecurePage 只能由已驗證的使用者存取的 。

† EF 主題 InMemory 測試說明如何使用記憶體內部資料庫搭配 MSTest 進行測試。 本主題使用 xUnit 測試架構。 不同測試架構的測試概念和測試實作很類似,但不相同。

雖然應用程式不使用存放庫模式,而且不是 工作單位 (UoW) 模式的有效範例, Razor 但 Pages 支援這些開發模式。 如需詳細資訊,請參閱 設計基礎結構持續性層測試控制器邏輯 , (範例實作存放庫模式) 。

測試應用程式組織

測試應用程式是目錄中的 tests/RazorPagesProject.Tests 主控台應用程式。

測試應用程式目錄 描述
AuthTests 包含下列專案的測試方法:
  • 由未經驗證的使用者存取安全頁面。
  • 使用模擬 AuthenticationHandler<TOptions> 的已驗證使用者存取安全頁面。
  • 取得GitHub使用者設定檔,並檢查設定檔的使用者登入。
BasicTests 包含路由和內容類型的測試方法。
IntegrationTests 包含使用自訂 WebApplicationFactory 類別之索引頁面的整合測試。
Helpers/Utilities
  • Utilities.cs 包含 InitializeDbForTests 用來以測試資料植入資料庫的方法。
  • HtmlHelpers.cs 提供方法可傳回 AngleSharp IHtmlDocument 以供測試方法使用。
  • HttpClientExtensions.cs 提供 多 SendAsync 載,以便將要求提交至 SUT。

測試架構為 xUnit。 整合測試是使用 Microsoft.AspNetCore.TestHost 進行,其中包含 TestServerMicrosoft.AspNetCore.Mvc.Testing因為套件是用來設定測試主機和測試伺服器,所以 TestHostTestServer 套件不需要測試應用程式專案檔或測試應用程式中開發人員組態中的直接套件參考。

在測試執行之前,整合測試通常需要資料庫中的小型資料集。 例如,刪除測試會呼叫刪除資料庫記錄,因此資料庫必須至少有一筆記錄,才能讓刪除要求成功。

範例應用程式會植入資料庫中有三則訊息 Utilities.cs ,測試可在執行時使用:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

SUT 的資料庫內容會在其 Startup.ConfigureServices 方法中註冊。 測試應用程式的 builder.ConfigureServices 回呼會在執行應用程式的 Startup.ConfigureServices 程式碼之後執行。 若要針對測試使用不同的資料庫,必須在 中 builder.ConfigureServices 取代應用程式的資料庫內容。 如需詳細資訊,請參閱 自訂 WebApplicationFactory 一節。

對於仍使用Web 主機的 SUT,測試應用程式的 builder.ConfigureServices 回呼會在 SUT Startup.ConfigureServices 的程式碼之前執行。 測試應用程式的 builder.ConfigureTestServices 回呼會在 之後執行。

其他資源