ASP.NET Core 中的整合測試

作者:Jos van der TilMartin CostelloJavier Calvarro Martin

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

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

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

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

若要測試 SPA,我們建議使用 適用于 .NET 的 Playwright之類的工具,以自動化瀏覽器。

整合測試簡介

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

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

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

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

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

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

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

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

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

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 主機不同。 例如,不同的資料庫或不同的應用程式設定可能會用於測試。

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

封裝 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 也會用於測試中。 請參閱 GitHub 中的專案檔

SUT 環境

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

預設 WebApplicationFactory 的基本測試

執行下列其中一項動作,將隱含定義的 Program 類別公開至測試專案:

  • 將 Web 應用程式的內部類型公開至測試專案。 這可以在 SUT 專案的檔案中完成, () .csproj

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Program 使用部分類別宣告將類別設為公用

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    
    public class BasicTests 
        : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly WebApplicationFactory<Program> _factory;
    
        public BasicTests(WebApplicationFactory<Program> 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());
        }
    }
    

    範例應用程式會使用 Program 部分類別方法。

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

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

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

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

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> 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());
    }
}

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

AngleSharp 與反分叉檢查的比較 Application Parts

本文使用 AngleSharp 剖析器,藉由載入頁面和剖析 HTML 來處理反分叉檢查。 若要在較低層級測試控制器和 Razor 頁面檢視的端點,而不需擔心它們在瀏覽器中呈現的方式,請考慮使用 Application Parts應用程式元件方法會將控制器或 Razor Page 插入應用程式,以用來提出 JS ON 要求以取得必要值。 如需詳細資訊,請參閱部落格整合測試 ASP.NET Core使用應用程式元件使用應用程式元件保護的資源,以及由 Martin Costello建立關聯的 GitHub 存放庫

自訂 WebApplicationFactory

Web 主機組態可以繼承自 WebApplicationFactory<TEntryPoint> 來建立一或多個自訂處理站,獨立建立測試類別:

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

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

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

    SUT 的資料庫內容已在 中 Program.cs 註冊。 測試應用程式的 builder.ConfigureServices 回呼會在執行應用程式的 Program.cs 程式碼之後執行。 若要對測試使用與應用程式資料庫不同的資料庫,必須在 中 builder.ConfigureServices 取代應用程式的資料庫內容。

    範例應用程式會尋找資料庫內容的服務描述元,並使用描述元來移除服務註冊。 然後,處理站會新增新的 ApplicationDbContext ,其會針對 test 使用記憶體內部資料庫。

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

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    
  2. 在測試類別中使用自訂 CustomWebApplicationFactory 。 下列範例會使用 類別中的 IndexPageTests Factory:

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> 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. 從回應剖析反移轉 cookie 和要求驗證權杖。
  3. 使用反移轉 cookie 和要求驗證權杖來提出 POST 要求。

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

  • GetDocumentAsync:接收 HttpResponseMessage 並傳 IHtmlDocument 回 。 GetDocumentAsync會使用根據原始 HttpResponseMessage 的 準備虛擬回應的處理站。 如需詳細資訊,請參閱 AngleSharp 檔
  • SendAsync 撰寫 HttpClientHttpRequestMessage 的擴充方法,並呼叫 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 。 如需詳細資訊,請參閱本文中的 AngleSharp 與 Application Parts 反forgery 檢查

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

使用 WithWebHostBuilder 自訂用戶端

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

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

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

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

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

用戶端選項

請參閱建立 WebApplicationFactoryClientOptionsHttpClient 實例時的預設值和可用選項頁面。

WebApplicationFactoryClientOptions建立 類別,並將其傳遞至 CreateClient() 方法:

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

插入模擬服務

您可以在測試中覆寫服務,並在主機產生器上呼叫 ConfigureTestServices

範例 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.");
    }
}

Program.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(
            "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, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

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

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

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

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

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

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

驗證中介軟體的基本測試

如需驗證中介軟體的基本測試,請參閱 此 GitHub 存放庫 。 它包含測試案例特有的 測試伺服器

設定環境

在自訂應用程式處理站中設定 環境

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

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

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 的資料庫內容已在 中 Program.cs 註冊。 測試應用程式的 builder.ConfigureServices 回呼會在執行應用程式的 Program.cs 程式碼之後執行。 若要針對測試使用不同的資料庫,必須在 中 builder.ConfigureServices 取代應用程式的資料庫內容。 如需詳細資訊,請參閱 自訂 WebApplicationFactory 一節。

其他資源

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

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

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

注意

針對測試 SPA,我們建議 使用適用于 .NET 的 Playwright之類的工具,以自動化瀏覽器。

整合測試簡介

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

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

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

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

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

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

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

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

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

ASP.NET Core整合測試

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

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

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

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

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

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

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

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

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

將單元測試與整合測試分隔成不同的專案。 分隔測試:

  • 協助確保基礎結構測試元件不會意外包含在單元測試中。
  • 允許控制要執行的一組測試。

頁面應用程式和 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 s,例如 TempData 提供者所使用的專案,請在測試中將它們標示為基本。 如需將 標示 cookie 為基本專案的指示,請參閱 Essential cookie s

自訂 WebApplicationFactory

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

  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 方法中註冊。 測試應用程式的 builder.ConfigureServices 回呼會在執行應用程式的 Startup.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. 從回應剖析反移轉 cookie 和要求驗證權杖。
  3. 使用反移轉 cookie 和要求驗證權杖來提出 POST 要求。

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

  • GetDocumentAsync:接收 HttpResponseMessage 並傳 IHtmlDocument 回 。 GetDocumentAsync會使用根據原始 HttpResponseMessage 的 準備虛擬回應的處理站。 如需詳細資訊,請參閱 AngleSharp 檔
  • SendAsync 撰寫 HttpClientHttpRequestMessage 的擴充方法,並呼叫 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 請使用 IWebHostBuilder 設定進一步自訂的 來建立新的 WebApplicationFactory

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

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

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

TestAuthHandler 驗證配置設定 Test 為 註冊的位置 AddAuthenticationConfigureTestServices 時,會呼叫 來驗證使用者。 配置必須 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 回呼會在 之後執行。

其他資源