[Blazor] 元件測試 - Service

11 mins.
  1. 1. 重構成 service
  2. 2. 修改測試
  3. 3. Mock Service
  4. 4. Reference

這是系列文的第三篇,今天要講到的是針對 Service 做測試。
如果還沒看過前面兩篇的話,請往這邊走 [Blazor] 元件測試 - 基礎篇[Blazor] 元件測試 - mock

使用上次的 repo 接續操作

重構成 service

在原本的 FetchData.razor 頁面中,是直接用 HttpClient 呼叫 API,今天的測試重點是 Service ,因此需要先將使用 HttpClient 的部分抽出成 Service。
建立資料夾 Services 及檔案 IDataService.cs

1
2
3
4
5
6
7
8
9
10
using System.Threading.Tasks;
using client.Pages;

namespace client.Services
{
public interface IDataService
{
public Task<FetchData.WeatherForecast[]?> GetWeatherForecast();
}
}

再建立一個檔案 DataService.cs 來繼承 IDataService,並且使用透過 DI 的方法取得 HttpClient。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using client.Pages;

namespace client.Services
{
public class DataService: IDataService
{
private HttpClient _http;

public DataService(HttpClient http)
{
_http = http;
}

public async Task<FetchData.WeatherForecast[]?> GetWeatherForecast()
{
return await _http.GetFromJsonAsync<FetchData.WeatherForecast[]>("sample-data/weather.json");
}
}
}

Blazor 要用到 Service 的話需要在 Program.cs 中宣告。

1
2
3
4
5
6
7
8
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped<IDataService, DataService>(); // <== 定義 DataService 及 IDataService 的關係
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();

最後一個步驟,就是將 FetchData.razor 改呼叫 Service。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@page "/fetchdata"
@using client.Services
@inject IDataService _dataService;

<!-- html 省略 -->

@code {
private WeatherForecast[]? forecasts;

protected override async Task OnInitializedAsync()
{
forecasts = await _dataService.GetWeatherForecast();
}

//...
}

修改測試

都改完以後先執行看看,確認功能跑起來是否正確,並且檢查測試案例是否通過,不出意料測試案例果然是失敗的,因為多了一個 Service。
Service 在 bUnit 中的做法跟 blazor 差不多,都是要加入 Services 的集合中。

1
2
3
4
5
6
7
8
9
10
[Test]
public void RenderWithoutResponse()
{
using var ctx = new Bunit.TestContext();
ctx.Services.AddScoped<IDataService, DataService>(); // <=== 定義 DataService 及 IDataService 的關係
var mock = ctx.Services.AddMockHttpClient();

var comp = ctx.RenderComponent<FetchData>();
StringAssert.Contains("Loading...", comp.Markup);
}

Mock Service

DataService 是繼承 Interface,有兩種做法可以使用

  • 建立另一個的 DataService 給測試使用
  • 使用 MOQ / NSubtitute

在前一篇已經安裝過 MOQ ,這邊就繼續採用他來作為測試的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Test]
public void MockService()
{
using var ctx = new Bunit.TestContext();

var mockService = new Mock<IDataService>();
mockService
.Setup(p => p.GetWeatherForecast())
.ReturnsAsync(new List<FetchData.WeatherForecast>
{
new() {Date = new DateTime(2022, 01, 20), TemperatureC = 15, Summary = "first data"}
}.ToArray());
ctx.Services.AddSingleton<IDataService>(mockService.Object);

var comp = ctx.RenderComponent<FetchData>();
comp.WaitForState(() => !comp.Markup.Contains("Loading..."));
Assert.IsNotNull(comp.Find(".table"));
}

透過 Mock Interface 的方法搭配上使用 AddSignleton。

Reference