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

Javier Calvarro NelsonSteve SmithJos van der 磚By Javier Calvarro Nelson, Steve Smith, and Jos van der Til

整合測試可確保應用程式的元件在包含應用程式支援基礎結構的層級(例如資料庫、檔案系統和網路)正常運作。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.

這些廣泛的測試是用來測試應用程式的基礎結構和整個架構,通常包括下列元件: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

單元測試使用製造的元件(稱為 fakesmock 物件)來取代基礎結構元件。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/模擬會導致更快速的測試執行。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.

本主題中使用 "SUT" 來參考經過測試的 ASP.NET Core 應用程式。"SUT" is used throughout this topic to refer to the tested ASP.NET Core app.

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. 測試專案具有此 SUT 的參考。The test project has a reference to the SUT.
  • 測試專案會建立適用于該 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 with the SUT.
  • 測試執行器會用來執行測試並報告測試結果。A test runner is used to execute the tests and report the test results.

整合測試會遵循一連串的事件,其中包含一般的 排列ActAssert 測試步驟: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) ,是由 AspNetCore 所提供,或由 測試 封裝所管理。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:

  • 從 d) (將相依性檔案從複製到測試專案的 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 類別,以簡化使用來啟動載入的工作 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頁面應用程式和 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 測試主控制器) 的元件整合。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. 檢查 測試/ Razor PagesProject. 測試/ Razor PagesProjectInspect 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:

測試中也會使用 Entity Framework Core。Entity Framework Core is also used in the tests. 應用程式參考:The app 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> 用來建立整合測試的TestServerWebApplicationFactory<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.

下列測試類別會 BasicTests 使用 WebApplicationFactory 來啟動載入並提供測試方法的 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. 如需將 a 標示 cookie 為必要的指示,請參閱 重要的 cookie sFor instructions on marking a cookie as essential, see Essential cookies.

自訂 WebApplicationFactoryCustomize WebApplicationFactory

您可以藉由繼承 WebApplicationFactory 來建立一或多個自訂處理站,以獨立于測試類別建立 Web 主機設定: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 =>
            {
                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 方法執行。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.

    已在其方法中註冊的 SUT 資料庫內容 Startup.ConfigureServicesThe SUT's database context is registered in its Startup.ConfigureServices method. 測試應用程式的 builder.ConfigureServices 回呼會在應用程式的程式 Startup.ConfigureServices 代碼執行之後執行。The test app's builder.ConfigureServices callback is executed after the app's Startup.ConfigureServices code is executed. 執行順序是 ASP.NET Core 3.0 版本之 泛型主機 的重大變更。The execution order is a breaking change for the Generic Host with the release of ASP.NET Core 3.0. 若要針對測試使用與應用程式資料庫不同的資料庫,則必須在中取代應用程式的資料庫內容 builder.ConfigureServicesTo use a different database for the tests than the app's database, the app's database context must be replaced in builder.ConfigureServices.

    對於仍在使用 Web 主機的 SUTs,測試應用程式的 builder.ConfigureServices 回呼會在 SUT 的程式碼 之前 執行 Startup.ConfigureServicesFor SUTs that still use the Web Host, the test app's builder.ConfigureServices callback is executed before the SUT's Startup.ConfigureServices code. 測試應用程式的 builder.ConfigureTestServices 回呼會 在之後 執行。The test app's builder.ConfigureTestServices callback is executed after.

    範例應用程式會尋找資料庫內容的服務描述項,並使用描述項來移除服務註冊。The sample app finds the service descriptor for the database context and uses the descriptor to remove the service registration. 接下來,factory 會加入新 ApplicationDbContext 的,其會使用記憶體內部資料庫進行測試。Next, the factory adds a new ApplicationDbContext that uses an in-memory database for the tests.

    若要連接到與記憶體內部資料庫不同的資料庫,請變更將 UseInMemoryDatabase 內容連接到不同資料庫的呼叫。To connect to a different database than the in-memory database, change the UseInMemoryDatabase call to connect the context to a different database. 若要使用 SQL Server 測試資料庫:To use a SQL Server test database:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. CustomWebApplicationFactory在測試類別中使用自訂。Use the custom CustomWebApplicationFactory in test classes. 下列範例會使用類別中的 factory IndexPageTestsThe 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. 如稍後在 Mock 驗證 區段中所述,這會允許測試檢查應用程式第一個回應的結果。As explained later in the Mock authentication section, this permits tests to check the result of the app's first response. 第一個回應是其中許多測試中有標頭的重新導向 LocationThe 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 系統自動進行的 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從回應中剖析 antiforgery 和要求驗證 token。Parse the antiforgery cookie and request validation token from the response.
  3. 使用 antiforgery cookie 和要求驗證權杖來發出 POST 要求。Make the POST request with the antiforgery cookie and request validation token in place.

SendAsyncHelper /) HttpClientExtensions 中的 helper 擴充方法 (協助程式/HtmlHelpers,以及 GetDocumentAsync 範例應用程式中的協助程式方法 (helper /) ,請使用 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 使用可根據原始的來準備 虛擬回應 的 factory HttpResponseMessageGetDocumentAsync uses a factory that prepares a virtual response based on the original HttpResponseMessage. 如需詳細資訊,請參閱 AngleSharp 檔For more information, see the AngleSharp documentation.
  • SendAsync``HttpClient撰寫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:
    • 表單 () 的 [提交] 按鈕 IHtmlElementSubmit 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. ASP.NET Core 應用程式的整合測試不支援或不需要 AngleSharp。AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. 您可以使用其他剖析器,例如 (HAP) 的 Html 敏捷套件 Other parsers can be used, such as the Html Agility Pack (HAP). 另一種方法是撰寫程式碼來處理 antiforgery 系統的要求驗證權杖,並 cookie 直接 antiforgery。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 其中包含由設定進一步自訂的 >iwebhostbuilderWhen 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 reseeded in this test method to ensure that a record is present for the SUT to delete. 在 sut 的 messages 要求中,會模擬在 sut 中選取表單的第一個 [刪除] 按鈕:Selecting the first delete 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.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);
}

用戶端選項Client options

下表顯示建立實例時可使用的預設 WebApplicationFactoryClientOptions HttpClientThe following table shows the default WebApplicationFactoryClientOptions available when creating HttpClient instances.

選項Option 描述Description 預設Default
Request.allowautoredirectAllowAutoRedirect 取得或設定 HttpClient 實例是否應該自動遵循重新導向回應。Gets or sets whether or not HttpClient instances should automatically follow redirect responses. true
BaseAddressBaseAddress 取得或設定實例的基底位址 HttpClientGets or sets the base address of HttpClient instances. http://localhost
句 Cookie 柄HandleCookies 取得或設定 HttpClient 實例是否應該處理 cookie s。Gets or sets whether HttpClient instances should handle cookies. true
MaxAutomaticRedirectionsMaxAutomaticRedirections 取得或設定實例應遵循的重新導向回應數目上限 HttpClientGets 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. 若要插入模擬服務,則必須具有 Startup 具有方法的類別 Startup.ConfigureServicesTo 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 .csServices/IQuoteService.cs:

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

Services/QuoteService .csServices/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 .csPages/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.">

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

IntegrationTests.IndexPageTests.csIntegrationTests.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.">

Mock 驗證Mock authentication

類別中的測試 AuthTests 會檢查安全的端點:Tests in the AuthTests class check that a secure endpoint:

  • 將未經驗證的使用者重新導向至應用程式的登入頁面。Redirects an unauthenticated user to the app's Login page.
  • 傳回已驗證使用者的內容。Returns content for an authenticated user.

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

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

Get_SecurePageRedirectsAnUnauthenticatedUser 測試中, WebApplicationFactoryClientOptions 會設定為 [不允許重新導向],方法是將 request.allowautoredirect 設定為 falseIn the Get_SecurePageRedirectsAnUnauthenticatedUser test, a WebApplicationFactoryClientOptions is set to disallow redirects by setting AllowAutoRedirect to false:

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

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

  • 在重新導向至登入頁面之後,您可以根據預期的 HttpStatusCode 來檢查您所傳回的狀態碼,而不是最後的狀態碼,這會是 HttpStatusCode。確定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.

測試應用程式可以在 AuthenticationHandler<TOptions> ConfigureTestServices 中模擬,以便測試驗證和授權的層面。The test app can mock an AuthenticationHandler<TOptions> in ConfigureTestServices in order to test aspects of authentication and authorization. 基本案例會傳回 AuthenticateResult。 SuccessA minimal scenario returns an 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 使用者 AddAuthentication ConfigureTestServicesThe TestAuthHandler is called to authenticate a user when the authentication scheme is set to Test where AddAuthentication is registered for ConfigureTestServices. Test配置必須符合您的應用程式所預期的配置是很重要的。It's important for the Test scheme to match the scheme your app expects. 否則,驗證將無法運作。Otherwise, authentication won't work.

[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 ,請參閱 用戶端選項 一節。For more information on WebApplicationFactoryClientOptions, see the Client options section.

設定環境Set the environment

根據預設,已將 SUT 的主機和應用程式環境設定為使用開發環境。By default, the SUT's host and app environment is configured to use the Development environment. 若要在使用時覆寫 SUT 的環境 IHostBuilderTo override the SUT's environment when using IHostBuilder:

  • 設定 ASPNETCORE_ENVIRONMENT 環境變數 (例如,、 Staging Production 或其他自訂值,例如 Testing) 。Set the ASPNETCORE_ENVIRONMENT environment variable (for example, Staging, Production, or other custom value, such as Testing).
  • CreateHostBuilder在測試應用程式中覆寫,以讀取前面加上的環境變數 ASPNETCOREOverride CreateHostBuilder in the test app to read environment variables prefixed with ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

如果 SUT 使用 () 的 Web 主機 IWebHostBuilder ,請覆寫 CreateWebHostBuilderIf the SUT uses the Web Host (IWebHostBuilder), override CreateWebHostBuilder:

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

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

此函式會藉 WebApplicationFactory 由在包含整合測試與元件相等的元件上搜尋WebApplicationFactoryContentRootAttribute ,來推斷應用程式內容的根路徑 TEntryPoint System.Reflection.Assembly.FullNameThe 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.

停用陰影複製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.jsThe sample app uses xUnit and disables shadow copying for xUnit by including an xunit.runner.json file with the correct configuration setting. 如需詳細資訊,請參閱 使用 JSON 設定 xUnitFor more information, see Configuring xUnit with JSON.

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

{
  "shadowCopy": false
}

物件的處置Disposal of objects

IClassFixture執行實施測試之後,當 xUnit 處置WebApplicationFactory時,會處置TestServerHttpClientAfter the tests of the IClassFixture implementation are executed, TestServer and HttpClient are disposed when xUnit disposes of the WebApplicationFactory. 如果開發人員所具現化的物件需要處置,請在執行時處置它們 IClassFixtureIf 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:

應用程式App 專案目錄Project directory 描述Description
(在 SUT) 的訊息應用程式Message app (the SUT) src/ Razor PagesProjectsrc/RazorPagesProject 允許使用者新增、刪除、刪除所有訊息,以及分析訊息。Allows a user to add, delete one, delete all, and analyze messages.
測試應用程式Test app 測試/ Razor PagesProjecttests/RazorPagesProject.Tests 用於整合測試的 SUT。Used to integration test the SUT.

您可以使用 IDE 的內建測試功能(例如 Visual Studio)來執行測試。The tests can be run using the built-in test features of an IDE, such as Visual Studio. 如果使用 Visual Studio Code 或命令列,請在 [ 測試/ Razor PagesProject ] 目錄中的命令提示字元執行下列命令: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 具有下列特性的頁面訊息系統:The SUT is a Razor Pages message system with the following characteristics:

  • 應用程式的 [索引] 頁面 (pages/index. cshtmlpages/index. CSHTML) 提供 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.

雖然應用程式不會使用存放庫模式,而不是 工作單元) (的有效範例,但 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

測試應用程式是 [ 測試/ Razor PagesProject ] 目錄中的主控台應用程式。The test app is a console app inside the tests/RazorPagesProject.Tests directory.

測試應用程式目錄Test app directory 描述Description
AuthTestsAuthTests 包含的測試方法:Contains test methods for:
  • 未經驗證的使用者存取安全的頁面。Accessing a secure page by an unauthenticated user.
  • 使用模擬以已驗證的使用者存取安全的頁面 AuthenticationHandler<TOptions>Accessing a secure page by an authenticated user with a mock AuthenticationHandler<TOptions>.
  • 取得 GitHub 使用者設定檔,並檢查設定檔的使用者登入。Obtaining a GitHub user profile and checking the profile's user login.
BasicTestsBasicTests 包含路由和內容類型的測試方法。Contains a test method for routing and content type.
IntegrationTestsIntegrationTests 包含使用自訂類別之 [索引] 頁面的整合測試 WebApplicationFactoryContains 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 會提供方法,以傳回 IHtmlDocument 測試方法所使用的 AngleSharp。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. 整合測試是使用 AspNetCore TestHost,其中包含 TestServerIntegration tests are conducted using the Microsoft.AspNetCore.TestHost, which includes the TestServer. 由於使用 AspNetCore 封裝來設定測試主機和測試伺服器,因此 TestHost 和套件不需要測試應用程式的專案檔 TestServer 或開發人員在測試應用程式中進行直接封裝參考。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 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.ConfigureServicesThe SUT's database context is registered in its Startup.ConfigureServices method. 測試應用程式的 builder.ConfigureServices 回呼會在應用程式的程式 Startup.ConfigureServices 代碼執行之後執行。The test app's builder.ConfigureServices callback is executed after the app's Startup.ConfigureServices code is executed. 若要使用不同的資料庫進行測試,必須在中取代應用程式的資料庫內容 builder.ConfigureServicesTo use a different database for the tests, the app's database context must be replaced in builder.ConfigureServices. 如需詳細資訊,請參閱 自訂 WebApplicationFactory 一節。For more information, see the Customize WebApplicationFactory section.

對於仍在使用 Web 主機的 SUTs,測試應用程式的 builder.ConfigureServices 回呼會在 SUT 的程式碼 之前 執行 Startup.ConfigureServicesFor SUTs that still use the Web Host, the test app's builder.ConfigureServices callback is executed before the SUT's Startup.ConfigureServices code. 測試應用程式的 builder.ConfigureTestServices 回呼會 在之後 執行。The test app's builder.ConfigureTestServices callback is executed after.

整合測試可確保應用程式的元件在包含應用程式支援基礎結構的層級(例如資料庫、檔案系統和網路)正常運作。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.

這些廣泛的測試是用來測試應用程式的基礎結構和整個架構,通常包括下列元件: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

單元測試使用製造的元件(稱為 fakesmock 物件)來取代基礎結構元件。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/模擬會導致更快速的測試執行。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.

本主題中使用 "SUT" 來參考經過測試的 ASP.NET Core 應用程式。"SUT" is used throughout this topic to refer to the tested ASP.NET Core app.

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. 測試專案具有此 SUT 的參考。The test project has a reference to the SUT.
  • 測試專案會建立適用于該 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 with the SUT.
  • 測試執行器會用來執行測試並報告測試結果。A test runner is used to execute the tests and report the test results.

整合測試會遵循一連串的事件,其中包含一般的 排列ActAssert 測試步驟: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) ,是由 AspNetCore 所提供,或由 測試 封裝所管理。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:

  • 從 d) (將相依性檔案從複製到測試專案的 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 類別,以簡化使用來啟動載入的工作 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頁面應用程式和 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 測試主控制器) 的元件整合。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. 檢查 測試/ Razor PagesProject. 測試/ Razor PagesProjectInspect 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> 用來建立整合測試的TestServerWebApplicationFactory<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.

下列測試類別會 BasicTests 使用 WebApplicationFactory 來啟動載入並提供測試方法的 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. 如需將 a 標示 cookie 為必要的指示,請參閱 重要的 cookie sFor instructions on marking a cookie as essential, see Essential cookies.

自訂 WebApplicationFactoryCustomize WebApplicationFactory

您可以藉由繼承 WebApplicationFactory 來建立一或多個自訂處理站,以獨立于測試類別建立 Web 主機設定: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設定服務集合,該集合會在應用程式之前執行 Startup.ConfigureServicesThe IWebHostBuilder allows the configuration of the service collection with ConfigureServices, which is executed before the app's Startup.ConfigureServices. >iwebhostbuilder可讓您使用ConfigureTestServices覆寫和修改服務集合中的已註冊服務:The IWebHostBuilder allows overriding and modifying registered services in the service collection with ConfigureTestServices:

    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. Error: {Message}", 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. 下列範例會使用類別中的 factory IndexPageTestsThe 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. 如稍後在 Mock 驗證 區段中所述,這會允許測試檢查應用程式第一個回應的結果。As explained later in the Mock authentication section, this permits tests to check the result of the app's first response. 第一個回應是其中許多測試中有標頭的重新導向 LocationThe 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 系統自動進行的 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從回應中剖析 antiforgery 和要求驗證 token。Parse the antiforgery cookie and request validation token from the response.
  3. 使用 antiforgery cookie 和要求驗證權杖來發出 POST 要求。Make the POST request with the antiforgery cookie and request validation token in place.

SendAsyncHelper /) HttpClientExtensions 中的 helper 擴充方法 (協助程式/HtmlHelpers,以及 GetDocumentAsync 範例應用程式中的協助程式方法 (helper /) ,請使用 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 使用可根據原始的來準備 虛擬回應 的 factory HttpResponseMessageGetDocumentAsync uses a factory that prepares a virtual response based on the original HttpResponseMessage. 如需詳細資訊,請參閱 AngleSharp 檔For more information, see the AngleSharp documentation.
  • SendAsync``HttpClient撰寫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:
    • 表單 () 的 [提交] 按鈕 IHtmlElementSubmit 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. ASP.NET Core 應用程式的整合測試不支援或不需要 AngleSharp。AngleSharp isn't supported or required for integration testing of ASP.NET Core apps. 您可以使用其他剖析器,例如 (HAP) 的 Html 敏捷套件 Other parsers can be used, such as the Html Agility Pack (HAP). 另一種方法是撰寫程式碼來處理 antiforgery 系統的要求驗證權杖,並 cookie 直接 antiforgery。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 其中包含由設定進一步自訂的 >iwebhostbuilderWhen 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 reseeded in this test method to ensure that a record is present for the SUT to delete. 在 sut 的 messages 要求中,會模擬在 sut 中選取表單的第一個 [刪除] 按鈕:Selecting the first delete 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.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);
}

用戶端選項Client options

下表顯示建立實例時可使用的預設 WebApplicationFactoryClientOptions HttpClientThe following table shows the default WebApplicationFactoryClientOptions available when creating HttpClient instances.

選項Option 描述Description 預設Default
Request.allowautoredirectAllowAutoRedirect 取得或設定 HttpClient 實例是否應該自動遵循重新導向回應。Gets or sets whether or not HttpClient instances should automatically follow redirect responses. true
BaseAddressBaseAddress 取得或設定實例的基底位址 HttpClientGets or sets the base address of HttpClient instances. http://localhost
句 Cookie 柄HandleCookies 取得或設定 HttpClient 實例是否應該處理 cookie s。Gets or sets whether HttpClient instances should handle cookies. true
MaxAutomaticRedirectionsMaxAutomaticRedirections 取得或設定實例應遵循的重新導向回應數目上限 HttpClientGets 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. 若要插入模擬服務,則必須具有 Startup 具有方法的類別 Startup.ConfigureServicesTo 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 .csServices/IQuoteService.cs:

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

Services/QuoteService .csServices/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 .csPages/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.">

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

IntegrationTests.IndexPageTests.csIntegrationTests.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.">

Mock 驗證Mock authentication

類別中的測試 AuthTests 會檢查安全的端點:Tests in the AuthTests class check that a secure endpoint:

  • 將未經驗證的使用者重新導向至應用程式的登入頁面。Redirects an unauthenticated user to the app's Login page.
  • 傳回已驗證使用者的內容。Returns content for an authenticated user.

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

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

Get_SecurePageRedirectsAnUnauthenticatedUser 測試中, WebApplicationFactoryClientOptions 會設定為 [不允許重新導向],方法是將 request.allowautoredirect 設定為 falseIn the Get_SecurePageRedirectsAnUnauthenticatedUser test, a WebApplicationFactoryClientOptions is set to disallow redirects by setting AllowAutoRedirect to false:

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

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

  • 在重新導向至登入頁面之後,您可以根據預期的 HttpStatusCode 來檢查您所傳回的狀態碼,而不是最後的狀態碼,這會是 HttpStatusCode。確定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.

測試應用程式可以在 AuthenticationHandler<TOptions> ConfigureTestServices 中模擬,以便測試驗證和授權的層面。The test app can mock an AuthenticationHandler<TOptions> in ConfigureTestServices in order to test aspects of authentication and authorization. 基本案例會傳回 AuthenticateResult。 SuccessA minimal scenario returns an 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 AddAuthentication ConfigureTestServicesThe TestAuthHandler is called to authenticate a user when the authentication scheme is set to Test where AddAuthentication is registered for ConfigureTestServices:

[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 ,請參閱 用戶端選項 一節。For more information on WebApplicationFactoryClientOptions, see the Client options section.

設定環境Set the environment

根據預設,已將 SUT 的主機和應用程式環境設定為使用開發環境。By default, the SUT's host and app environment is configured to use the Development environment. 若要覆寫 SUT 的環境:To override the SUT's environment:

  • 設定 ASPNETCORE_ENVIRONMENT 環境變數 (例如,、 Staging Production 或其他自訂值,例如 Testing) 。Set the ASPNETCORE_ENVIRONMENT environment variable (for example, Staging, Production, or other custom value, such as Testing).
  • CreateWebHostBuilder在測試應用程式中覆寫以讀取 ASPNETCORE_ENVIRONMENT 環境變數。Override CreateWebHostBuilder in the test app to read the ASPNETCORE_ENVIRONMENT environment variable.
public class CustomWebApplicationFactory<TStartup> 
    : WebApplicationFactory<TStartup> where TStartup: class
{
    protected override IWebHostBuilder CreateWebHostBuilder()
    {
        return base.CreateWebHostBuilder()
            .UseEnvironment(
                Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"));
    }

    ...
}

您也可以直接在自訂中的主機建立器上設定環境 WebApplicationFactory<TEntryPoint>The environment can also be set directly on the host builder in a custom WebApplicationFactory<TEntryPoint>:

public class CustomWebApplicationFactory<TStartup> 
    : WebApplicationFactory<TStartup> where TStartup: class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment(
            Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"));

        ...
    }

    ...

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

此函式會藉 WebApplicationFactory 由在包含整合測試與元件相等的元件上搜尋WebApplicationFactoryContentRootAttribute ,來推斷應用程式內容的根路徑 TEntryPoint System.Reflection.Assembly.FullNameThe 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.

停用陰影複製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.jsThe sample app uses xUnit and disables shadow copying for xUnit by including an xunit.runner.json file with the correct configuration setting. 如需詳細資訊,請參閱 使用 JSON 設定 xUnitFor more information, see Configuring xUnit with JSON.

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

{
  "shadowCopy": false
}

如果使用 Visual Studio,請將檔案的 [ 複製到輸出目錄 ] 屬性設定為 [ 永遠複製]。If using Visual Studio, set the file's Copy to Output Directory property to Copy always. 如果未使用 Visual Studio,請將 Content 目標新增至測試應用程式的專案檔:If not using Visual Studio, add a Content target to the test app's project file:

<ItemGroup>
  <Content Update="xunit.runner.json">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </Content>
</ItemGroup>

物件的處置Disposal of objects

IClassFixture執行實施測試之後,當 xUnit 處置WebApplicationFactory時,會處置TestServerHttpClientAfter the tests of the IClassFixture implementation are executed, TestServer and HttpClient are disposed when xUnit disposes of the WebApplicationFactory. 如果開發人員所具現化的物件需要處置,請在執行時處置它們 IClassFixtureIf 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:

應用程式App 專案目錄Project directory 描述Description
(在 SUT) 的訊息應用程式Message app (the SUT) src/ Razor PagesProjectsrc/RazorPagesProject 允許使用者新增、刪除、刪除所有訊息,以及分析訊息。Allows a user to add, delete one, delete all, and analyze messages.
測試應用程式Test app 測試/ Razor PagesProjecttests/RazorPagesProject.Tests 用於整合測試的 SUT。Used to integration test the SUT.

您可以使用 IDE 的內建測試功能(例如 Visual Studio)來執行測試。The tests can be run using the built-in test features of an IDE, such as Visual Studio. 如果使用 Visual Studio Code 或命令列,請在 [ 測試/ Razor PagesProject ] 目錄中的命令提示字元執行下列命令: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 具有下列特性的頁面訊息系統:The SUT is a Razor Pages message system with the following characteristics:

  • 應用程式的 [索引] 頁面 (pages/index. cshtmlpages/index. CSHTML) 提供 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.

雖然應用程式不會使用存放庫模式,而不是 工作單元) (的有效範例,但 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

測試應用程式是 [ 測試/ Razor PagesProject ] 目錄中的主控台應用程式。The test app is a console app inside the tests/RazorPagesProject.Tests directory.

測試應用程式目錄Test app directory 描述Description
AuthTestsAuthTests 包含的測試方法:Contains test methods for:
  • 未經驗證的使用者存取安全的頁面。Accessing a secure page by an unauthenticated user.
  • 使用模擬以已驗證的使用者存取安全的頁面 AuthenticationHandler<TOptions>Accessing a secure page by an authenticated user with a mock AuthenticationHandler<TOptions>.
  • 取得 GitHub 使用者設定檔,並檢查設定檔的使用者登入。Obtaining a GitHub user profile and checking the profile's user login.
BasicTestsBasicTests 包含路由和內容類型的測試方法。Contains a test method for routing and content type.
IntegrationTestsIntegrationTests 包含使用自訂類別之 [索引] 頁面的整合測試 WebApplicationFactoryContains 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 會提供方法,以傳回 IHtmlDocument 測試方法所使用的 AngleSharp。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. 整合測試是使用 AspNetCore TestHost,其中包含 TestServerIntegration tests are conducted using the Microsoft.AspNetCore.TestHost, which includes the TestServer. 由於使用 AspNetCore 封裝來設定測試主機和測試伺服器,因此 TestHost 和套件不需要測試應用程式的專案檔 TestServer 或開發人員在測試應用程式中進行直接封裝參考。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 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." }
    };
}

其他資源Additional resources