ASP.NET Core 中的整合測試Integration tests in ASP.NET Core

藉由Luke LathamSteve SmithBy Luke Latham and Steve Smith

整合測試可確保應用程式的元件正確運作,其中包含應用程式的支援基礎結構,例如資料庫、 檔案系統和網路層級。Integration tests ensure that an app's components function correctly at a level that includes the app's supporting infrastructure, such as the database, file system, and network. ASP.NET Core 支援使用測試 web 主機和記憶體測試伺服器中的單元測試架構的整合測試。ASP.NET Core supports integration tests using a unit test framework with a test web host and an in-memory test server.

本主題假設您對單元測試知識有基本了解。This topic assumes a basic understanding of unit tests. 如果不熟悉測試概念,請參閱 .NET Core 與 .NET Standard 中的單元測試主題和其連結的內容。If unfamiliar with test concepts, see the Unit Testing in .NET Core and .NET Standard topic and its linked content.

檢視或下載範例程式碼 (英文) (如何下載)View or download sample code (how to download)

範例應用程式是 Razor 頁面應用程式,並假設 Razor 頁面的基本知識。The sample app is a Razor Pages app and assumes a basic understanding of Razor Pages. 如果不熟悉使用 Razor 頁面,請參閱下列主題:If unfamiliar with Razor Pages, see the following topics:

注意

進行測試的 Spa,我們建議工具這類Selenium,它可以自動在瀏覽器。For testing SPAs, we recommended a tool such as Selenium, which can automate a browser.

整合測試的簡介Introduction to integration tests

整合測試評估比更廣泛的層級上的應用程式的元件單元測試Integration tests evaluate an app's components on a broader level than unit tests. 單元測試用來測試隔離的軟體元件,例如個別類別方法。Unit tests are used to test isolated software components, such as individual class methods. 整合測試確認兩個或多個應用程式元件一起運作以產生預期的結果,可能包括 完整處理要求所需的每個元件。Integration tests confirm that two or more app components work together to produce an expected result, possibly including every component required to fully process a request.

這些更廣泛的測試用來測試應用程式的基礎結構和整個 framework,通常包括下列元件:These broader tests are used to test the app's infrastructure and whole framework, often including the following components:

  • 資料庫Database
  • 檔案系統File system
  • 網路設備Network appliances
  • 要求-回應管線Request-response pipeline

單元測試使用傳遞元件,稱為fakes或是模擬物件,來取代基礎結構元件。Unit tests use fabricated components, known as fakes or mock objects, in place of infrastructure components.

相較於單元測試,整合測試:In contrast to unit tests, integration tests:

  • 使用實際的應用程式在生產環境中使用的元件。Use the actual components that the app uses in production.
  • 需要更多的程式碼及資料處理。Require more code and data processing.
  • 需要較長的時間。Take longer to run.

因此,限制使用的最重要的基礎結構案例的整合測試。Therefore, limit the use of integration tests to the most important infrastructure scenarios. 如果可以使用單元測試或整合測試,測試行為,請選擇 單元測試。If a behavior can be tested using either a unit test or an integration test, choose the unit test.

提示

不要撰寫每個可能的排列的資料與檔案存取資料庫和檔案系統的整合測試。Don't write integration tests for every possible permutation of data and file access with databases and file systems. 不論多少位數跨應用程式會與資料庫和檔案系統、 特定的一份讀取、 寫入、 更新和刪除的整合測試通常能夠適當測試資料庫和檔案系統元件互動。Regardless of how many places across an app interact with databases and file systems, a focused set of read, write, update, and delete integration tests are usually capable of adequately testing database and file system components. 使用單元測試的方法的邏輯與這些元件互動的例行性測試。Use unit tests for routine tests of method logic that interact with these components. 在單元測試中的基礎結構使用 fakes/模擬 (mock) 更快速的測試執行中的結果。In unit tests, the use of infrastructure fakes/mocks result in faster test execution.

注意

在討論中的整合測試,測試的專案時經常會呼叫待測系統,或簡稱為「 SUT」。In discussions of integration tests, the tested project is frequently called the system under test, or "SUT" for short.

ASP.NET Core 整合測試ASP.NET Core integration tests

ASP.NET Core 中的整合測試需要下列各項:Integration tests in ASP.NET Core require the following:

  • 測試專案用來包含及執行測試。A test project is used to contain and execute the tests. 測試專案已測試的 ASP.NET Core 專案,稱為參考待測系統(SUT)。The test project has a reference to the tested ASP.NET Core project, called the system under test (SUT). 「 SUT 」 在本主題中用以參考該測試的應用程式。"SUT" is used throughout this topic to refer to the tested app.
  • 測試專案建立 SUT 測試 web 主機,並使用測試伺服器用戶端處理要求和回應 SUT。The test project creates a test web host for the SUT and uses a test server client to handle requests and responses to the SUT.
  • 測試執行器用來執行測試和報告測試結果。A test runner is used to execute the tests and report the test results.

整合測試會遵循包含一般的事件序列排列Act,並Assert測試步驟:Integration tests follow a sequence of events that include the usual Arrange, Act, and Assert test steps:

  1. SUT 的 web 主機已設定。The SUT's web host is configured.
  2. 測試伺服器用戶端會提交要求給應用程式。A test server client is created to submit requests to the app.
  3. 排列測試步驟執行:測試應用程式會準備要求。The Arrange test step is executed: The test app prepares a request.
  4. Act測試步驟執行:用戶端會提交要求,並接收回應。The Act test step is executed: The client submits the request and receives the response.
  5. Assert測試步驟執行:實際回應會驗證為傳遞或是失敗根據預期回應。The Assert test step is executed: The actual response is validated as a pass or fail based on an expected response.
  6. 處理程序會繼續直到所有的測試會執行。The process continues until all of the tests are executed.
  7. 報告測試結果。The test results are reported.

通常,測試 web 主機設定以不同的方式比執行測試的應用程式的一般的 web 主機。Usually, the test web host is configured differently than the app's normal web host for the test runs. 比方說,不同的資料庫或不同的應用程式設定可能會用來測試。For example, a different database or different app settings might be used for the tests.

基礎結構元件,例如測試 web 主機和記憶體測試伺服器 (TestServer)、 提供或由Microsoft.AspNetCore.Mvc.Testing封裝。Infrastructure components, such as the test web host and in-memory test server (TestServer), are provided or managed by the Microsoft.AspNetCore.Mvc.Testing package. 使用此封裝可以簡化測試建立和執行。Use of this package streamlines test creation and execution.

Microsoft.AspNetCore.Mvc.Testing封裝處理下列工作:The Microsoft.AspNetCore.Mvc.Testing package handles the following tasks:

  • 將相依性檔案複製 ( *.deps) 從到測試專案的 SUT bin目錄。Copies the dependencies file (*.deps) from the SUT into the test project's bin directory.
  • 設定 SUT 專案根目錄的內容根目錄,因此,靜態檔案和頁面/檢視表位於測試執行時。Sets the content root to the SUT's project root so that static files and pages/views are found when the tests are executed.
  • 提供WebApplicationFactory類別來簡化啟動程序與 SUT TestServerProvides the WebApplicationFactory class to streamline bootstrapping the SUT with TestServer.

單元測試文件說明如何設定測試專案和測試執行器,以及如何執行測試和建議方法,名稱測試和測試類別的詳細指示。The unit tests documentation describes how to set up a test project and test runner, along with detailed instructions on how to run tests and recommendations for how to name tests and test classes.

注意

建立測試專案的應用程式時,分開的單元測試整合測試分成不同的專案。When creating a test project for an app, separate the unit tests from the integration tests into different projects. 這有助於確保測試基礎結構的元件意外不包含在單元測試。This helps ensure that infrastructure testing components aren't accidentally included in the unit tests. 區隔單元和整合測試也可讓控制哪些資料集的測試的執行。Separation of unit and integration tests also allows control over which set of tests are run.

沒有幾乎任何的 Razor Pages 應用程式的測試組態和 MVC 應用程式之間的差異。There's virtually no difference between the configuration for tests of Razor Pages apps and MVC apps. 唯一的差別是在測試命名的方式。The only difference is in how the tests are named. 在 Razor 頁面應用程式中測試的頁面端點通常命名為之後的頁面模型類別 (例如IndexPageTests來測試索引頁面的元件整合)。In a Razor Pages app, tests of page endpoints are usually named after the page model class (for example, IndexPageTests to test component integration for the Index page). 在 MVC 應用程式中,測試會通常依控制器類別並命名這些測試的控制站 (例如HomeControllerTests測試針對 Home 控制器元件整合)。In an MVC app, tests are usually organized by controller classes and named after the controllers they test (for example, HomeControllerTests to test component integration for the Home controller).

測試應用程式的必要條件Test app prerequisites

測試專案必須:The test project must:

這些必要條件中所見範例應用程式These prerequisites can be seen in the sample app. 檢查tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj檔案。Inspect the tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj file. 範例應用程式會使用xUnit測試架構和AngleSharp剖析器程式庫,因此範例應用程式也會參考:The sample app uses the xUnit test framework and the AngleSharp parser library, so the sample app also references:

SUT 環境SUT environment

如果 SUT環境未設定,則環境會預設為開發。If the SUT's environment isn't set, the environment defaults to Development.

預設值 WebApplicationFactory 的基本測試Basic tests with the default WebApplicationFactory

WebApplicationFactory<TEntryPoint> 用來建立TestServer整合測試。WebApplicationFactory<TEntryPoint> is used to create a TestServer for the integration tests. TEntryPoint 通常是 SUT 的進入點類別Startup類別。TEntryPoint is the entry point class of the SUT, usually the Startup class.

測試類別會實作類別的裝置介面 (IClassFixture) 以指出包含測試類別,且提供共用的物件執行個體的類別中的測試。Test classes implement a class fixture interface (IClassFixture) to indicate the class contains tests and provide shared object instances across the tests in the class.

基本測試的應用程式端點Basic test of app endpoints

下列測試類別, BasicTests,使用WebApplicationFactorySUT 啟動程序,並提供HttpClient至測試方法, Get_EndpointsReturnSuccessAndCorrectContentTypeThe following test class, BasicTests, uses the WebApplicationFactory to bootstrap the SUT and provide an HttpClient to a test method, Get_EndpointsReturnSuccessAndCorrectContentType. 方法會檢查是否成功的回應狀態碼 (在 200-299 的範圍內的狀態碼) 和Content-Type標頭是text/html; charset=utf-8數個應用程式頁面。The method checks if the response status code is successful (status codes in the range 200-299) and the Content-Type header is text/html; charset=utf-8 for several app pages.

CreateClient建立的執行個體HttpClient,會自動遵循重新導向,並會處理 cookie。CreateClient creates an instance of HttpClient that automatically follows redirects and handles cookies.

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

根據預設,不會保留非必要 cookie 的要求時GDPR 同意原則已啟用。By default, non-essential cookies aren't preserved across requests when the GDPR consent policy is enabled. 若要保留非必要的 cookie,例如所使用的 TempData 提供者,請將它們標示為在測試中不可或缺。To preserve non-essential cookies, such as those used by the TempData provider, mark them as essential in your tests. 如需標示為基本的 cookie 的指示,請參閱 < 不可或缺的 cookieFor instructions on marking a cookie as essential, see Essential cookies.

測試安全的端點Test a secure endpoint

中的另一個測試BasicTests類別會檢查安全端點重新導向未驗證的使用者,應用程式的登入頁面。Another test in the BasicTests class checks that a secure endpoint redirects an unauthenticated user to the app's Login page.

SUT,在/SecurePage頁面上使用AuthorizePage來套用慣例AuthorizeFilter至頁面。In the SUT, the /SecurePage page uses an AuthorizePage convention to apply an AuthorizeFilter to the page. 如需詳細資訊,請參閱 Razor Pages 授權慣例For more information, see Razor Pages authorization conventions.

services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AuthorizePage("/SecurePage");
    });

Get_SecurePageRequiresAnAuthenticatedUser測試,請WebApplicationFactoryClientOptions設為禁止重新導向設定AllowAutoRedirectfalse:In the Get_SecurePageRequiresAnAuthenticatedUser test, a WebApplicationFactoryClientOptions is set to disallow redirects by setting AllowAutoRedirect to false:

[Fact]
public async Task Get_SecurePageRequiresAnAuthenticatedUser()
{
    // 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);
}

不允許用戶端以遵循重新導向,您可以進行下列檢查:By disallowing the client to follow the redirect, the following checks can be made:

  • SUT 所傳回的狀態碼可以檢查對預期HttpStatusCode.Redirect結果,不登入頁面,會重新導向後的最終狀態程式碼HttpStatusCode.OK.The status code returned by the SUT can be checked against the expected HttpStatusCode.Redirect result, not the final status code after the redirect to the Login page, which would be HttpStatusCode.OK.
  • Location回應標頭中的標頭值會檢查以確認它開頭http://localhost/Identity/Account/Login,不是最後登入頁面的回應,其中Location標頭不會出現。The Location header value in the response headers is checked to confirm that it starts with http://localhost/Identity/Account/Login, not the final Login page response, where the Location header wouldn't be present.

如需詳細資訊WebApplicationFactoryClientOptions,請參閱 用戶端選項一節。For more information on WebApplicationFactoryClientOptions, see the Client options section.

自訂 WebApplicationFactoryCustomize WebApplicationFactory

Web 主機組態可以建立獨立的測試類別,藉由繼承自WebApplicationFactory建立一或多個自訂的處理站:Web host configuration can be created independently of the test classes by inheriting from WebApplicationFactory to create one or more custom factories:

  1. 繼承自WebApplicationFactory,並覆寫ConfigureWebHostInherit from WebApplicationFactory and override ConfigureWebHost. IWebHostBuilder可讓服務集合的組態ConfigureServices:The IWebHostBuilder allows the configuration of the service collection with ConfigureServices:

    public class CustomWebApplicationFactory<TStartup> 
        : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                // Create a new service provider.
                var serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();
    
                // Add a database context (ApplicationDbContext) using an in-memory 
                // database for testing.
                services.AddDbContext<ApplicationDbContext>(options => 
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                    options.UseInternalServiceProvider(serviceProvider);
                });
    
                // Build the service provider.
                var sp = services.BuildServiceProvider();
    
                // Create a scope to obtain a reference to the database
                // context (ApplicationDbContext).
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
    
                    // Ensure the database is created.
                    db.Database.EnsureCreated();
    
                    try
                    {
                        // Seed the database with test data.
                        Utilities.InitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding the " +
                            $"database with test messages. Error: {ex.Message}");
                    }
                }
            });
        }
    }
    

    在植入的資料庫範例應用程式由執行InitializeDbForTests方法。Database seeding in the sample app is performed by the InitializeDbForTests method. 中所述的方法是整合測試的範例:測試應用程式組織一節。The method is described in the Integration tests sample: Test app organization section.

  2. 使用自訂CustomWebApplicationFactory測試類別中。Use the custom CustomWebApplicationFactory in test classes. 下列範例會使用中的處理站IndexPageTests類別:The following example uses the factory in the IndexPageTests class:

    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從下列重新導向。The sample app's client is configured to prevent the HttpClient from following redirects. 中所述測試安全的端點 區段中,這可讓測試,以檢查應用程式的第一個回應的結果。As explained in the Test a secure endpoint section, this permits tests to check the result of the app's first response. 第一個回應是,有許多使用這些測試中的重新導向Location標頭。The first response is a redirect in many of these tests with a Location header.

  3. 一般測試會使用HttpClient和 helper 方法來處理要求和回應:A typical test uses the HttpClient and helper methods to process the request and the response:

    [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 要求都必須滿足的應用程式時,會自動成為 antiforgery 核取資料保護防偽系統Any POST request to the SUT must satisfy the antiforgery check that's automatically made by the app's data protection antiforgery system. 若要排列測試的 POST 要求,測試應用程式必須:In order to arrange for a test's POST request, the test app must:

  1. 提出要求的頁面。Make a request for the page.
  2. 剖析的防偽 cookie 和回應的要求驗證權杖。Parse the antiforgery cookie and request validation token from the response.
  3. 在位置進行 POST 要求,使用防偽 cookie 和要求驗證權杖。Make the POST request with the antiforgery cookie and request validation token in place.

SendAsync協助程式的擴充方法 (Helpers/HttpClientExtensions.cs) 和GetDocumentAsynchelper 方法 (Helpers/HtmlHelpers.cs) 中的範例應用程式使用AngleSharp剖析器來處理 antiforgery 檢查使用下列方法:The SendAsync helper extension methods (Helpers/HttpClientExtensions.cs) and the GetDocumentAsync helper method (Helpers/HtmlHelpers.cs) in the sample app use the AngleSharp parser to handle the antiforgery check with the following methods:

  • GetDocumentAsync – 接收HttpResponseMessage ,並傳回IHtmlDocumentGetDocumentAsync – Receives the HttpResponseMessage and returns an IHtmlDocument. GetDocumentAsync 使用準備的處理站虛擬回應根據原始HttpResponseMessageGetDocumentAsync uses a factory that prepares a virtual response based on the original HttpResponseMessage. 如需詳細資訊,請參閱 AngleSharp 文件For more information, see the AngleSharp documentation.
  • SendAsync 擴充方法HttpClientcompose HttpRequestMessage然後呼叫SendAsync(HttpRequestMessage)提交要求給 SUT。SendAsync extension methods for the HttpClient compose an HttpRequestMessage and call SendAsync(HttpRequestMessage) to submit requests to the SUT. 多載SendAsync接受 HTML 表單 (IHtmlFormElement) 和下列:Overloads for SendAsync accept the HTML form (IHtmlFormElement) and the following:
    • 提交按鈕的表單 (IHtmlElement)Submit button of the form (IHtmlElement)
    • 表單值集合 (IEnumerable<KeyValuePair<string, string>>)Form values collection (IEnumerable<KeyValuePair<string, string>>)
    • 提交按鈕 (IHtmlElement),從而形成值 (IEnumerable<KeyValuePair<string, string>>)Submit button (IHtmlElement) and form values (IEnumerable<KeyValuePair<string, string>>)

注意

AngleSharp第三方剖析供示範之用,本主題中的範例應用程式的程式庫。AngleSharp is a third-party parsing library used for demonstration purposes in this topic and the sample app. AngleSharp 不支援,或整合測試的 ASP.NET Core 應用程式所需。AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. 其他剖析器可以使用,例如Html 靈活度組件 (HAP)Other parsers can be used, such as the Html Agility Pack (HAP). 另一種方法是撰寫程式碼來直接處理防偽系統的要求驗證語彙基元和防偽 cookie。Another approach is to write code to handle the antiforgery system's request verification token and antiforgery cookie directly.

自訂 WithWebHostBuilder 的用戶端Customize the client with WithWebHostBuilder

在測試方法中,需要額外的設定時WithWebHostBuilder新建WebApplicationFactory具有IWebHostBuilder ,進一步自訂組態。When additional configuration is required within a test method, WithWebHostBuilder creates a new WebApplicationFactory with an IWebHostBuilder that is further customized by configuration.

Post_DeleteMessageHandler_ReturnsRedirectToRoot測試方法範例應用程式示範如何使用WithWebHostBuilderThe Post_DeleteMessageHandler_ReturnsRedirectToRoot test method of the sample app demonstrates the use of WithWebHostBuilder. 這項測試觸發 SUT 中的送出表單,在資料庫中執行記錄刪除。This test performs a record delete in the database by triggering a form submission in the SUT.

因為另一個測試中IndexPageTests類別執行作業會刪除所有資料庫中的記錄,並可能執行之前Post_DeleteMessageHandler_ReturnsRedirectToRoot方法中,資料庫就會在這個測試方法,以確保有一筆記錄的刪除 SUT 植入。Because another test in the IndexPageTests class performs an operation that deletes all of the records in the database and may run before the Post_DeleteMessageHandler_ReturnsRedirectToRoot method, the database is seeded in this test method to ensure that a record is present for the SUT to delete. 選取deleteBtn1按鈕的messagesSUT 中的表單模擬 SUT 的要求中:Selecting the deleteBtn1 button of the messages form in the SUT is simulated in the request to the 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.InitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding " +
                            "the database with test messages. Error: " +
                            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("button[id='deleteBtn1']"));

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

用戶端選項Client options

下表顯示的預設WebApplicationFactoryClientOptions建立時可以使用HttpClient執行個體。The following table shows the default WebApplicationFactoryClientOptions available when creating HttpClient instances.

選項Option 描述Description 預設Default
AllowAutoRedirectAllowAutoRedirect 取得或設定是否HttpClient執行個體應該會自動遵循重新導向回應。Gets or sets whether or not HttpClient instances should automatically follow redirect responses. true
BaseAddressBaseAddress 取得或設定基底位址HttpClient執行個體。Gets or sets the base address of HttpClient instances. http://localhost
HandleCookiesHandleCookies 取得或設定是否HttpClient執行個體應該處理 cookie。Gets or sets whether HttpClient instances should handle cookies. true
MaxAutomaticRedirectionsMaxAutomaticRedirections 取得或設定最大重新導向回應數目HttpClient應該遵循執行個體。Gets or sets the maximum number of redirect responses that HttpClient instances should follow. 77

建立WebApplicationFactoryClientOptions類別,並將它傳遞給CreateClient方法 (預設值在程式碼範例所示):Create the WebApplicationFactoryClientOptions class and pass it to the CreateClient method (default values are shown in the code example):

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

插入模擬 (mock) 的服務Inject mock services

服務可以藉由呼叫測試中覆寫ConfigureTestServices主應用程式產生器。Services can be overridden in a test with a call to ConfigureTestServices on the host builder. 若要插入模擬 (mock) 的服務,必須有 SUTStartup類別搭配Startup.ConfigureServices方法。To inject mock services, the SUT must have a Startup class with a Startup.ConfigureServices method.

此範例 SUT 包含範圍的服務會傳回報價。The sample SUT includes a scoped service that returns a quote. 要求 索引 頁面時索引 頁面上的隱藏欄位中內嵌引號。The quote is embedded in a hidden field on the Index page when the Index page is requested.

Services/IQuoteService.cs:Services/IQuoteService.cs:

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

Services/QuoteService.cs: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.csStartup.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.csPages/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:Pages/Index.cs:

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

SUT 應用程式執行時,會產生下列標記:The following markup is generated when the SUT app is run:

<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.">

若要測試服務和引號插入在整合測試,模擬 (mock) 的服務會插入 SUT 測試。To test the service and quote injection in an integration test, a mock service is injected into the SUT by the test. 模擬 (mock) 的服務會取代應用程式的QuoteService測試應用程式所提供的服務,使用稱為TestQuoteService:The mock service replaces the app's QuoteService with a service provided by the test app, called TestQuoteService:

IntegrationTests.IndexPageTests.cs: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 呼叫時,並已設定領域的服務註冊:ConfigureTestServices is called, and the scoped service is registered:

[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,因此判斷提示的階段:The markup produced during the test's execution reflects the quote text supplied by TestQuoteService, thus the assertion passes:

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

如何測試基礎結構會推斷的應用程式內容根路徑How the test infrastructure infers the app content root path

WebApplicationFactory建構函式會藉由搜尋來推斷應用程式內容根路徑WebApplicationFactoryContentRootAttribute等於索引鍵包含整合測試的組件上TEntryPoint的組件System.Reflection.Assembly.FullName.The WebApplicationFactory constructor infers the app content root path by searching for a WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal to the TEntryPoint assembly System.Reflection.Assembly.FullName. 如果找不到具有正確金鑰的屬性,WebApplicationFactory回到搜尋解決方案檔案 ( *.sln),並將附加TEntryPoint的方案目錄的組件名稱。In case an attribute with the correct key isn't found, WebApplicationFactory falls back to searching for a solution file (*.sln) and appends the TEntryPoint assembly name to the solution directory. 應用程式根目錄 (內容根路徑) 用來探索檢視和內容檔案。The app root directory (the content root path) is used to discover views and content files.

在大部分情況下,不需要明確地將應用程式內容根目錄設定,因為通常在執行階段尋找正確的內容根目錄的搜尋邏輯。In most cases, it isn't necessary to explicitly set the app content root, as the search logic usually finds the correct content root at runtime. 未找到的內容根目錄的特殊案例中使用內建的搜尋演算法,內容可以指定根目錄,明確或使用自訂的邏輯應用程式。In special scenarios where the content root isn't found using the built-in search algorithm, the app content root can be specified explicitly or by using custom logic. 若要設定應用程式的內容根目錄,在這些情況下,呼叫UseSolutionRelativeContentRoot擴充方法,從Microsoft.AspNetCore.TestHost封裝。To set the app content root in those scenarios, call the UseSolutionRelativeContentRoot extension method from the Microsoft.AspNetCore.TestHost package. 提供方案的相對路徑和選擇性的方案檔案名稱或 glob 模式 (預設 = *.sln)。Supply the solution's relative path and optional solution file name or glob pattern (default = *.sln).

呼叫UseSolutionRelativeContentRoot擴充方法使用一個下列其中一個方法:Call the UseSolutionRelativeContentRoot extension method using ONE of the following approaches:

  • 設定測試類別時WebApplicationFactory,提供的自訂組態IWebHostBuilder:When configuring test classes with WebApplicationFactory, provide a custom configuration with the IWebHostBuilder:

    public IndexPageTests(
         WebApplicationFactory<RazorPagesProject.Startup> factory)
    {
         var _factory = factory.WithWebHostBuilder(builder =>
         {
             builder.UseSolutionRelativeContentRoot("<SOLUTION-RELATIVE-PATH>");
    
             ...
         });
    }
    
  • 使用自訂設定測試類別時WebApplicationFactory,繼承自WebApplicationFactory,並覆寫ConfigureWebHost:When configuring test classes with a custom WebApplicationFactory, inherit from WebApplicationFactory and override ConfigureWebHost:

    public class CustomWebApplicationFactory<TStartup>
         : WebApplicationFactory<RazorPagesProject.Startup>
    {
         protected override void ConfigureWebHost(IWebHostBuilder builder)
         {
             builder.ConfigureServices(services =>
             {
                 builder.UseSolutionRelativeContentRoot("<SOLUTION-RELATIVE-PATH>");
    
                 ...
             });
         }
    }
    

停用陰影複製Disable shadow copying

陰影複製會導致在不同的目錄,比輸出目錄中執行測試。Shadow copying causes the tests to execute in a different directory than the output directory. 針對測試才能正常運作,陰影複製必須停用。For tests to work properly, shadow copying must be disabled. 範例應用程式xunit,並停用所包括的 xUnit 陰影複製xunit.runner.json檔案使用的是正確的組態設定。The sample app uses xUnit and disables shadow copying for xUnit by including an xunit.runner.json file with the correct configuration setting. 如需詳細資訊,請參閱 設定使用 JSON 的 xUnit.netFor more information, see Configuring xUnit with JSON.

新增xunit.runner.json檔案根目錄的測試專案,使用下列內容:Add the xunit.runner.json file to root of the test project with the following content:

{
  "shadowCopy": false
}

物件的處置Disposal of objects

在測試後IClassFixture實作會執行, TestServerHttpClient xUnit 處置時進行處置WebApplicationFactory.After the tests of the IClassFixture implementation are executed, TestServer and HttpClient are disposed when xUnit disposes of the WebApplicationFactory. 如果物件具現化開發人員需要處置,處置它們在IClassFixture實作。If objects instantiated by the developer require disposal, dispose of them in the IClassFixture implementation. 如需詳細資訊,請參閱 < 實作 Dispose 方法For more information, see Implementing a Dispose method.

整合測試範例Integration tests sample

範例應用程式是兩個應用程式所組成:The sample app is composed of two apps:

AppApp 專案目錄Project directory 描述Description
訊息應用程式 (SUT)Message app (the SUT) src/RazorPagesProjectsrc/RazorPagesProject 允許使用者加入、 刪除其中一個、 全部刪除,以及分析訊息。Allows a user to add, delete one, delete all, and analyze messages.
測試應用程式Test app tests/RazorPagesProject.Teststests/RazorPagesProject.Tests 用來整合測試 SUT。Used to integration test the SUT.

可以使用內建的測試功能的 IDE,例如執行測試Visual StudioThe tests can be run using the built-in test features of an IDE, such as Visual Studio. 如果使用Visual Studio Code或命令列中,執行下列命令在命令提示字元中tests/RazorPagesProject.Tests目錄:If using Visual Studio Code or the command line, execute the following command at a command prompt in the tests/RazorPagesProject.Tests directory:

dotnet test

訊息應用程式 (SUT) 組織Message app (SUT) organization

SUT 是 Razor Pages 訊息系統具有下列特性:The SUT is a Razor Pages message system with the following characteristics:

  • 應用程式的 [索引] 頁面 (pages/Index.cshtmlPages/Index.cshtml.cs) 提供 UI 和頁面模型的方法,來控制新增、 刪除和分析的訊息 (每個訊息的平均字).The Index page of the app (Pages/Index.cshtml and Pages/Index.cshtml.cs) provides a UI and page model methods to control the addition, deletion, and analysis of messages (average words per message).
  • 所描述的訊息Message類別 (Data/Message.cs) 使用兩個屬性: Id (索引鍵) 和Text(訊息)。A message is described by the Message class (Data/Message.cs) with two properties: Id (key) and Text (message). Text屬性是必要的且限制為 200 個字元。The Text property is required and limited to 200 characters.
  • 使用儲存的訊息Entity Framework 的記憶體中的資料庫†。Messages are stored using Entity Framework's in-memory database†.
  • 應用程式在其資料庫內容類別中,包含資料存取層 (DAL) AppDbContext (Data/AppDbContext.cs)。The app contains a data access layer (DAL) in its database context class, AppDbContext (Data/AppDbContext.cs).
  • 如果資料庫是空的應用程式啟動時,就會使用三個訊息初始化訊息存放區。If the database is empty on app startup, the message store is initialized with three messages.
  • 應用程式包含/SecurePage,只能存取已驗證使用者。The app includes a /SecurePage that can only be accessed by an authenticated user.

†EF 主題使用 InMemory 進行測試,說明如何使用記憶體中資料庫的 mstest 執行測試。†The EF topic, Test with InMemory, explains how to use an in-memory database for tests with MSTest. 本主題會使用xUnit測試架構。This topic uses the xUnit test framework. 測試概念和跨不同的測試架構的測試實作都類似,但不是完全相同。Test concepts and test implementations across different test frameworks are similar but not identical.

雖然應用程式不會使用儲存機制模式,而且不是有效的範例工作單位 (UoW) 模式,Razor 頁面支援的開發這些模式。Although the app doesn't use the repository pattern and isn't an effective example of the Unit of Work (UoW) pattern, Razor Pages supports these patterns of development. 如需詳細資訊,請參閱 < 設計基礎結構持續層測試控制器邏輯(此範例會實作儲存機制模式)。For more information, see Designing the infrastructure persistence layer and Test controller logic (the sample implements the repository pattern).

測試應用程式的組織Test app organization

測試應用程式是主控台應用程式內tests/RazorPagesProject.Tests目錄。The test app is a console app inside the tests/RazorPagesProject.Tests directory.

測試應用程式目錄Test app directory 描述Description
BasicTestsBasicTests BasicTests.cs包含路由、 存取安全的頁面未驗證的使用者,並取得 GitHub 使用者設定檔和檢查設定檔的使用者登入的測試方法。BasicTests.cs contains test methods for routing, accessing a secure page by an unauthenticated user, and obtaining a GitHub user profile and checking the profile's user login.
IntegrationTestsIntegrationTests IndexPageTests.cs包含 [索引] 頁面使用自訂的整合測試WebApplicationFactory類別。IndexPageTests.cs contains the integration tests for the Index page using custom WebApplicationFactory class.
協助程式/公用程式Helpers/Utilities
  • Utilities.cs包含InitializeDbForTests方法用來植入測試資料的資料庫。Utilities.cs contains the InitializeDbForTests method used to seed the database with test data.
  • HtmlHelpers.cs提供方法來傳回 AngleSharpIHtmlDocument以供測試方法。HtmlHelpers.cs provides a method to return an AngleSharp IHtmlDocument for use by the test methods.
  • HttpClientExtensions.cs提供的多載SendAsync提交要求給 SUT。HttpClientExtensions.cs provide overloads for SendAsync to submit requests to the SUT.

測試架構xUnitThe test framework is xUnit. 使用執行整合測試Microsoft.AspNetCore.TestHost,其中包括TestServerIntegration tests are conducted using the Microsoft.AspNetCore.TestHost, which includes the TestServer. 因為Microsoft.AspNetCore.Mvc.Testing封裝用來設定測試主應用程式和測試伺服器TestHostTestServer套件不需要在測試應用程式的專案檔中直接將封裝參考或在測試應用程式的開發人員設定。Because the Microsoft.AspNetCore.Mvc.Testing package is used to configure the test host and test server, the TestHost and TestServer packages don't require direct package references in the test app's project file or developer configuration in the test app.

植入資料庫中的測試Seeding the database for testing

整合測試通常需要在測試執行前的資料庫中的小型資料集。Integration tests usually require a small dataset in the database prior to the test execution. 比方說,刪除測試會呼叫資料庫記錄的刪除,所以資料庫必須至少一個記錄才會成功的刪除要求。For example, a delete test calls for a database record deletion, so the database must have at least one record for the delete request to succeed.

範例應用程式會植入資料庫中的三個訊息Utilities.cs它們執行時,可以使用測試:The sample app seeds the database with three messages in Utilities.cs that tests can use when they execute:

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

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

其他資源Additional resources