BlazorでのbUnitテスト

C#

画面の挙動をアプリと同じようにテストできないか調べたところ、「bUnit」というテストライブラリを見つけました。本記事では、bUnitを試した結果を紹介します。

bUnitについて

BlazorはC#を用いたWebアプリケーションフレームワークであり、コンポーネント単位での開発が可能です。bUnitは、そのBlazorコンポーネントを単体テストするためのテストライブラリで、UIの挙動や表示内容をコードで検証できます。

bUnitを使用すると、次のようなメリットがあります。

  • コンポーネントの動作を自動テストできる
  • ユーザー操作(クリックや入力)をシミュレーションできる
  • モックを活用し、API通信を伴うコンポーネントのテストが可能

本記事の内容は、bUnitの公式ページをもとに実施しています。詳細は以下をご覧ください。

前提

以下の環境で動作確認を行っています。

oswindows11
.NET8.0.304
UIBlazor(WebAssembly standalone)
IDEVisual Studio 2022 Community
Bootstrap5.3.3

また、以下を前提としています:

  • Blazorプロジェクト(Bootstrap導入済み)を作成済み。
  • ソースコードは説明に必要な部分のみを抜粋。
  • ユニットテストの基礎を理解している。

テスト用のサンプル画面の作成

まず、テスト対象となるサンプル画面を作成します。以下のようなシンプルなユーザー検索画面を用意しました。

上記画面のコードは本記事の最後に掲載しています。

bUnitの設定

bUnitBlazorのコンポーネントをユニットテストするためのライブラリです。セットアップとして、bUnitをプロジェクトに追加し、必要な依存関係を導入します。

画面上に表示されているテキストのテスト

サンプル画面のボタンのテキストやメッセージの表示を確認するテストを作成します。

public class BUnitTest : TestContext
{
	[Fact]
	public void SampleTest()
	{
		// Arrange
		var cut = RenderComponent<Sample>();
		var button = cut.Find("button");
		var result = cut.Find("#result");

		// Assert
		Assert.Equal("検索", button.InnerHtml);
		Assert.Equal("検索してください", result.InnerHtml);
	}
}

テストの大まかな実施の流れは以下のようになります。

  • RenderComponent<T>メソッドを使い、テスト対象のコンポーネントをレンダリング
  • Findメソッドを使い、ボタンやテキスト要素を取得
  • Assert.Equalメソッドで期待する表示内容と比較

Findメソッドでは、HTMLタグ、idclass名など様々な方法で対象の要素を取得できます。

パラメータを渡したテスト

サンプル画面では、メッセージをパラメータとして渡すことで初期メッセージを指定できます。テストでもパラメータを設定して動作を検証します。

	[Fact]
	public void ParameterTest()
	{
		// Arange
		var cut = RenderComponent<Sample>(p => p.Add(m => m.FirstMessage, "test message"));
		var result = cut.Find("#result");

		// Assert
		Assert.Equal("test message", result.InnerHtml);
	}

テスト内容は先ほどと同じですが、レンダリング時にパラメータを渡しています。

フォームのSubmitのテスト

フォームに情報を入力してボタンをクリックすると、APIから取得したデータを表示する処理をテストします。

RichardSzalay.MockHttpの追加

サンプル画面では HttpClient を使用してAPI通信を行いますが、bUnit単体では HttpClient のモックがサポートされていません。そのため、RichardSzalay.MockHttp というライブラリを利用します。

テスト実施

	[Fact]
	public void MethodTest()
	{
		// Arrange
		var mock = Services.AddMockHttpClient();
		var response = new List<Person> { new Person { Name = "Test", Country = "Brazil" } };
		var jsonResponse = JsonSerializer.Serialize(response);
		mock.When("https://localhost:7013/users*").Respond("application/json", jsonResponse);
		var cut = RenderComponent<Sample>();
		var button = cut.Find("button");

		// Act
		button.Click();
		cut.Render();
		var contents = cut.FindAll(".col-2");

		// Assert
		Assert.NotEmpty(contents);
		Assert.Equal("Test", contents[2].InnerHtml);
		Assert.Equal("Brazil", contents[3].InnerHtml);
	}

Arrangeの内容を簡単に説明します。DIを行うためにはServicesに対してサービスを登録します。ServicesはテストクラスにTestContextを継承することで利用できます。Program.csでのIServiceCollectionと同じように使うことができます。AddMockHttpClient公式ページで紹介されているServicesの拡張メソッドです。

public static class MockHelper
{
	public static MockHttpMessageHandler AddMockHttpClient(this TestServiceProvider services)
	{
		var mockHttpHandler = new MockHttpMessageHandler();
		var httpClient = mockHttpHandler.ToHttpClient();
		services.AddSingleton<HttpClient>(httpClient);
		return mockHttpHandler;
	}
}

続いて、mockのWhenRespondメソッドを用いて、モックの動作を設定します。メソッド名の通りですが、Whenでモック動作の条件を指定して、Respondメソッドに条件が満たされたときのレスポンスの値を指定します。

その後、ボタンをクリックし、最新のレンダリング結果を取得して検証します。

まとめ

本記事では、BlazorのコンポーネントをbUnitを使ってテストする方法を紹介しました。

  • bUnitを利用すると、コンポーネントのレンダリング結果やイベント処理をテストできる
  • RenderComponent<T>メソッドを使用してコンポーネントを作成し、ボタンやメッセージの表示を検証
  • MockHttpを利用することで、HTTP通信を行うコンポーネントのテストも可能

サンプル画面のコード

@inject ILogger<Sample> logger
@inject HttpClient httpClient

<h3>ユーザー情報</h3>

<EditForm Model="InputForm" OnSubmit="GetUsersAsync">
	<div class="form-control">
		<label for="countrySelect">国</label>
		<InputSelect id="countrySelect" class="form-select" @bind-Value="InputForm.Country">
			<option value="Japan">日本</option>
			<option value="US">アメリカ</option>
			<option value="Italy">イタリア</option>
			<option value="Frnce">フランス</option>
		</InputSelect>
	</div>
	<button class="btn btn-primary ms-3 mt-1">検索</button>
</EditForm>

<div id="result" class="container mt-3">
	@if (People is not null && People.Any())
	{
		<div class="row">
			<div class="col-2 border border-black">名前</div>
			<div class="col-2 border border-black">国</div>
		</div>
		@foreach (var person in People)
		{
			<div class="row">
				<div class="col-2 border border-black">@person.Name</div>
				<div class="col-2 border border-black">@person.Country</div>
			</div>
		}
	}
	else
	{
		@message
	}
</div>

@code {
	[Parameter]
	public string? FirstMessage { get; set; }

	private Form InputForm { get; set; } = new();
	private List<Person>? People;
	private string? message;

	protected override void OnInitialized()
	{
		message = FirstMessage ?? "検索してください";
	}

	private async Task GetUsersAsync()
	{
		logger.LogInformation($"country={InputForm.Country}");

		var r = await httpClient.GetAsync($"https://localhost:7013/users?country={InputForm.Country}");
		var j = await r.Content.ReadAsStringAsync();
		People = JsonSerializer.Deserialize<List<Person>>(j);

		if (People is null || !People.Any()) message = "該当のユーザーは存在しません";
	}

	private class Form
	{
		public string? Country { get; set; } = "Japan";
	}
}

コメント