在Blazor中測試。一個完整的教程

語言: CN / TW / HK

簡介

在C#開發中引入Blazor,使開發人員有能力將其開發擴充套件到瀏覽器中,而無需依賴React、Vue.js和Angular等傳統JavaScript框架。

雖然在傳統的JavaScript框架中設定測試比較容易,但Blazor需要將一些工具和包整合在一起,然後瞭解如何以及在應用程式中測試什麼。

這篇文章將介紹如何為一個簡單的Blazor計數器應用程式設定測試,並將其擴充套件到包括C#開發人員可能想在Blazor應用程式中測試的幾乎所有內容。

建立一個測試環境

首先,我們來設定演示專案。

建立一個新專案

在Visual Studio中,點選 "新建"。

Screenshot of Visual Studio homepage with arrow pointing to the "+new" button

Web和控制檯選單中,選擇應用程式,然後選擇Blazor伺服器應用程式

Visual Studio new project page

在下一頁中,繼續不進行驗證,然後設定專案名稱解決方案名稱。單擊 "建立"。

設定一個測試專案

要設定一個測試專案,從檔案選單的下拉選單中點選新建解決方案;應該會彈出一個模板視窗。

從左側邊欄的Web 控制檯組,選擇測試,選擇xUnit測試專案,然後點選下一步

Visual Studio new test project page

使用與主專案相同的框架版本,然後點選下一步

Configure new xUnit test project page

最後,為解決方案和專案設定一個名稱,然後點選建立

一旦完成,你的Visual Studio應該有如下的側邊欄。

Visual Studio with Blazor sample app set up

將主專案連結到測試專案

為了使測試專案能夠引用和使用主專案,我們必須在測試專案中建立一個連結,這樣我們就可以從主專案中匯入和使用元件、類和介面。

在Visual Studio裡面,從左邊的側邊欄中右擊測試解決方案,選擇編輯專案檔案,然後在同一組裡面新增<ProjectReference Include="../path/to/main-project/main-project.csproj" /> ,並新增SDK版本。

Gif of inputting code to link the main and test Blazor apps

設定測試依賴性

安裝bUnit

專案選單中,點選管理NuGet包,搜尋bUnit,選擇bUnitbUnit.core,點選新增包,選擇兩個解決方案,然後點選OK

Gif of bUnit being added to project

安裝xUnit

這個測試專案被引導為xUnit專案。預設情況下,它自帶xUnit包。

安裝Moq

Moq是一個斷言庫,對於測試預期結果是否與返回的結果相匹配很有用。

我們可以用安裝bUnit的同樣方法來安裝Moq。只需搜尋並選擇Moq,點選新增包,選擇測試專案,然後點選OK

用bUnit測試

xUnit是一個測試框架,它提供了一個介面,可以在瀏覽器之外執行Blazor應用程式,並仍然通過程式碼與輸出進行互動。

bUnit是一個介面,我們可以通過它與Blazor元件互動。bUnit提供的介面使我們有可能在Blazor元件上觸發事件,找到元件上的一些元素,並作出斷言。

測試設定

要用bUnit測試Blazor應用程式,測試套件必須在測試專案中的一個類中有一個測試用例功能。

測試用例中的程式碼應該有以下內容。

  • Arrange, 設定一個TestContext (一個用於渲染Blazor元件的虛擬環境)。
  • Act ,將一個元件渲染到測試環境中,觸發動作,並提出網路請求。
  • Assert ,檢查事件是否被觸發以及是否顯示了正確的文字。

作為一個例子,下面的設定說明了上述步驟。

``` using BlazorApp.Pages; using Bunit; using Xunit;

namespace BlazorAppTests { public class CounterTest { [Fact] public void RendersSuccessfully() {

        using var ctx = new TestContext();

        // Render Counter component.
        var component = ctx.RenderComponent<Counter>();

        // Assert: first, find the parent_name vital element, then verify its content.
        Assert.Equal("Click me", component.Find($".btn").TextContent);
    }

}

}

```

從右邊的側邊欄,點選測試,然後點選全部執行來執行這個測試。

Gif of running a test in Blazor

傳遞引數給元件

有時,元件需要引數才能正確呈現。bUnit 提供了一個介面來處理這個問題。

首先,讓我們修改應用程式解決方案中的counter 元件,使其看起來像下面這樣。

``` @page "/counter/{DefaultCount:int?}"

Counter

Current count: @currentCount

@code { private int currentCount = 0;

[Parameter]
public int DefaultCount { get; set; }

protected override void OnParametersSet()
{
    if (DefaultCount != 0)
    {
        currentCount = DefaultCount;
    }
}

private void IncrementCount()
{
    currentCount++;
}

}

```

首先,注意到我們如何更新了路徑,以接受一個DefaultCount 的引數,即一個整數。? 告訴Blazor,這個引數是可選的,對元件的執行不是必需的。

接下來,注意到C#程式碼中的DefaultCount 屬性有一個[Parameter] 屬性。我們已經將OnParametersSet 生命週期方法掛起,以便在引數被設定時通知元件。這確保我們用它來更新元件currentValue 屬性,而不是讓元件從零開始計數。

我們可以在bUnit測試用例中用以下方法渲染這個元件。

``` using BlazorApp.Pages; using Bunit; using Xunit;

namespace BlazorAppTests { public class CounterTest { public void RendersSuccessfully() {

        using var ctx = new TestContext();

        Action onBtnClickHandler = () => { };

        // Render Counter component.
        var component = ctx.RenderComponent<Counter>(
          parameters =>
            parameters
                  // Add parameters
              .Add(c => c.DefaultCount, 10)
              .Add(c => c.OnBtnClick, onBtnClickHandler)
        );


        // Assert: first find the parent_name strong element, then verify its content.
        Assert.Equal("Click me", component.Find($".btn").TextContent);
    }

}

}

```

在上面的測試中的第14行,我們渲染元件,然後傳遞一個回撥給元件,呼叫(p => );

然後,我們將Add 方法新增到引數(p => p.Add(c => c.DefaultCount, 10); ,以便將該引數設定為10。

我們可以用同樣的方法傳遞一個事件回撥,即p.Add(c => c.onBtnClickHandler, onBtnClickHandler) 。這樣,我們在onBtnClickHandler 動作中實現了計數器的遞增,而不是在counter 元件中。

將輸入和服務傳遞給元件

有些元件依靠外部服務來執行,而有些則依靠外部欄位。我們可以通過測試上下文中的Services.AddSingleton 方法,用bUnit來實現這一點。

在演示的計數器應用裡面,有一個FetchData.razor 檔案,它嚴重依賴一個WeatherForecastService 服務。讓我們嘗試在xUnit測試專案中執行這個檔案。

在測試專案中建立一個名為FetchDataTest.cs 的新檔案,並新增以下內容。

``` using System; using BlazorApp.Data; using BlazorApp.Pages; using Bunit; using Microsoft.Extensions.DependencyInjection; using Xunit;

namespace BlazorAppTests { public class FetchDataTest { [Fact] public void RendersSuccessfully() {

        using var ctx = new TestContext();

        ctx.Services.AddSingleton<WeatherForecastService>(new WeatherForecastService());

        // Render Counter component.
        var component = ctx.RenderComponent<FetchData>();

        Assert.Equal("Weather forecast", component.Find($"h1").TextContent);
    }

}

}

```

注意我們是如何使用AddSingleton 介面來新增一個新的服務到我們的測試執行器上下文的。而當我們執行這個測試檔案時,我們應該得到一個成功的結果。

事件

上面,我們看到了如何在測試用例元件內為一個事件設定回撥。讓我們看看如何在元件內的一個元素上觸發事件。

計數器測試檔案有一個按鈕,當點選時,會增加計數器。讓我們測試一下,確保我們可以點選這個按鈕,看到頁面上的計數更新。

在測試專案中的CounterTest.cs 檔案內,在CounterTest 測試套件類中新增以下測試案例。

``` [Fact] public void ButtonClickAndUpdatesCount() { // Arrange using var ctx = new TestContext(); var component = ctx.RenderComponent();

// Render
var counterValue = "0";
Assert.Equal(counterValue, component.Find($"#counterVal").TextContent);

counterValue = "1";
var buttonElement = component.Find("button");

buttonElement.Click();

Assert.Equal(counterValue, component.Find($"#counterVal").TextContent);

}

```

在 "排列 "部分設定了該元件。像往常一樣,在 "Render "部分,我們首先斷言該元件從零開始。

然後,我們使用測試上下文元件的.Find 介面獲得按鈕的引用,這時返回元素的引用,它也有一些像Click() 方法的API。

最後,我們斷言元件的值,以確認按鈕的點選會做同樣的動作。

等待非同步的狀態更新

請注意,在注入服務後,我們沒有測試是否有任何資料被渲染。就像FetchData.razor 元件一樣,有些元件需要時間才能渲染出正確的資料。

我們可以通過component.waitForState(fn, duration) 方法來等待非同步狀態的更新。

``` [Fact] public void RendersServiceDataSuccessfully() {

using var ctx = new TestContext();

ctx.Services.AddSingleton<WeatherForecastService>(new WeatherForecastService());

// Render Counter component.
var component = ctx.RenderComponent<FetchData>();

component.WaitForState(() => component.Find(".date").TextContent == "Date");


Assert.Equal("TABLE", component.Find($".table").NodeName);

}

```

上面的例子等待非同步資料的載入,直到WaitForState 中的匿名函式被呼叫,該函式測試找到一個具有date 類的元素。一旦找到了,我們就可以對結果做一些進一步的斷言。

驗證標記

我們還可以通過MarkupMatches bUnit介面方法驗證一個元件的標記是否遵循相同的模式。

例如,我們可以測試索引是否包含有 "Hello, World!"文字內容的h1

首先,在測試專案內建立一個新檔案,命名為IndexTest.cs ,並新增以下內容。

``` using System; using BlazorApp.Pages; using Bunit; using Xunit;

namespace BlazorAppTests { public class IndexTest { [Fact] public void RendersSuccessfully() {

        using var ctx = new TestContext();

        // Act
        var component = ctx.RenderComponent<BlazorApp.Pages.Index>();

        // Assert
        Assert.Equal("Hello, world!", component.Find($"h1").TextContent);
    }

}

}

```

除此以外,我們還可以通過.Find (我們已經在這樣做了),和FindAll ,來驗證一個元件是否包含一個元素,它可以返回所有與查詢相匹配的特徵。這些方法採用了類似於CSS的選擇器,這使我們更容易遍歷節點。

嘲弄IJSRuntime

IJSRuntime是一個介面,它使得從.Net程式碼中與JavaScript互動成為可能。

一些元件可能依賴於它;例如,一個元件可以使用jQuery方法來進行API呼叫。

如果我們的專案中有JavaScript函式getPageTitle ,我們可以模擬該函式的呼叫,這樣在我們元件的任何地方,其結果將是我們在測試案例中可能指定的。

``` using var ctx = new TestContext();

ctx.Services.AddSingleton(new WeatherForecastService());

var theResult = "some result"; ctx.JSInterop.Setup("getPageTitme").SetResult(theResult);

// Render Counter component. var component = ctx.RenderComponent();

Assert.Equal(theResult, component.Find($".page-title").TextContent);

```

嘲弄HttpClient

一些應用程式依靠來自遠端伺服器的資料來正常執行。

單元測試的部分策略是使每個測試用例的依賴性不受影響。而依靠HTTP客戶端接觸到遠端伺服器的元件來呈現一個功能,如果結果不是靜態的,就會破壞我們的測試。

我們可以通過模擬HTTPClient來消除這個問題,HTTPClient是一個可以從Blazor應用內部向外部世界發出HTTP請求的庫。

根據bUnit的文件,bUnit預設不包含這個功能,但我們可以依靠第三方庫來實現這個功能。

首先,將RichardSzalay.MockHttp包新增到測試專案中。

``` dotnet add package RichardSzalay.MockHttp --version 6.0.0

```

接下來,在測試專案的根部建立一個名為MockHttpClientBunitHelpers 的檔案,並新增以下內容。

``` using Bunit; using Microsoft.Extensions.DependencyInjection; using RichardSzalay.MockHttp; using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json;

public static class MockHttpClientBunitHelpers { public static MockHttpMessageHandler AddMockHttpClient(this TestServiceProvider services) { var mockHttpHandler = new MockHttpMessageHandler(); var httpClient = mockHttpHandler.ToHttpClient(); httpClient.BaseAddress = new Uri("http://localhost"); services.AddSingleton(httpClient); return mockHttpHandler; }

public static MockedRequest RespondJson<T>(this MockedRequest request, T content)
{
    request.Respond(req =>
    {
        var response = new HttpResponseMessage(HttpStatusCode.OK);
        response.Content = new StringContent(JsonSerializer.Serialize(content));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        return response;
    });
    return request;
}

public static MockedRequest RespondJson<T>(this MockedRequest request, Func<T> contentProvider)
{
    request.Respond(req =>
    {
        var response = new HttpResponseMessage(HttpStatusCode.OK);
        response.Content = new StringContent(JsonSerializer.Serialize(contentProvider()));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        return response;
    });
    return request;
}

}

```

現在,建立一個新的測試案例,並新增以下內容。

``` [Fact] public void FetchResultTest() { var serverTime = "1632114204"; using var ctx = new TestContext(); var mock = ctx.Services.AddMockHttpClient(); mock.When("/getTime").RespondJson(serverTime);

// Render Counter component.
var component = ctx.RenderComponent<FetchData>();

Assert.Equal(serverTime, component.Find($".time").TextContent);

}

```

在這裡,我們聲明瞭一個變數,用來儲存我們對伺服器的期望,然後通過一個bUnit輔助方法ctx.Services.AddMockHttpClient ,將模擬的客戶端新增到上下文服務中,該方法將尋找MockHttpClientBunitHelpers ,並將其注入到上下文。

然後,我們使用模擬的引用來模擬我們期望從路由中得到的響應。最後,我們斷言我們元件的一部分具有我們從模擬請求返回的值。

總結

在這篇文章中,我們看到了如何設定一個Blazor專案並新增另一個xUnit測試專案。我們還將bUnit作為一個測試框架,並討論了使用bUnit來測試Blazor元件。

除了xUnit作為一個測試框架外,bUnit還可以在nUnit測試框架中使用類似的概念和API執行。

在這篇文章中,我們介紹了bUnit的一般用法。高階用法可在bUnit文件網站上找到。

The postTesting in Blazor:完整的教程》首先出現在LogRocket部落格上。