はじめに
WPF ( Blazor Hybrid )アプリをどこまで自動テストできるのか試してみました ![]()
-
想定読者
- Blazor Hybrid + WPF に興味があるが、テスト方法がわからない人
- WPFアプリでもWeb技術を使ったUIテストを試してみたい人
- xUnitやbUnitでのテスト実践例を見たい人
-
今回の結論
-
xUnitでロジックの試験は通常通り書ける - Blazorコンポーネントの単体試験は
bUnitで書ける(複雑な例でも行けるかは不明) -
Playwright for .NETでのE2E試験は、現状WPFのBlazorWebViewへの接続方法が分からなかったため実施できなかった- 黒魔術的なセットアップをすればできるのかも…?(調査に時間がかかりそうですが)
-
-
検証環境
- OS: Windows 11
- エディタ: VSCode
- .NET SDK: 9.0
- 使用ライブラリ: bUnit, xUnit
ソリューションとプロジェクトの用意
まずは空のソリューションを作成します。
mkdir BlazorWpfAppWithTest
cd BlazorWpfAppWithTest
dotnet new sln -n BlazorWpfAppWithTest
WPFアプリを用意
WPFアプリも用意しますが、せっかくなので前回使ったアプリをそのまま流用します。
実務においては試験しやすいプロジェクト構成・フォルダ構成にしたほうが良いと思いますが、今回は簡単な検証用途なのでそのまま使います。
ソリューションに追加します。
dotnet sln BlazorWpfAppWithTest.sln add BlazorWpfApp
テストプロジェクトを用意
ロジックやUIテスト用にテストプロジェクトを作ります。bUnitの公式ドキュメントに記載されている方法に従って次の手順を踏みます。
- bUnitのテンプレートを用意
- ソリューションへ参照追加
- 細かい部分の修正
- WPFアプリへの参照を追加
- さらに細かい修正
手順1~2は次のコマンドで実施できます。
dotnet new --install bunit.template
dotnet new bunit --framework xunit -o Tests
dotnet sln BlazorWpfAppWithTest.sln add Tests
このままだと、流用してきたWPFアプリのフレームワークnet9.0-windowsと互換性が無かったり、usingが足りずにビルドエラーが出るので修正します。
テストプロジェクトのcsprojのターゲットフレームワークをnet9.0-windowsに変更します。
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
...略
WPFアプリへの参照を追加します。
cd Tests
dotnet add reference ../BlazorWpfApp/BlazorWpfApp.csproj
_Imports.razorに以下のusingを追加して、UIテスト用のファイルでusingを都度追加しなくても良いようにしておきます。
@* WPFアプリ側で定義したBlazorコンポーネントの名前空間 *@
@using BlazorWpfApp.Components
この状態でテンプレートには次の二つのファイルがあります。
Counter.razorCounterCSharpTests.cs
CounterCSharpTests.csを開いて次のようにしてみます。
using BlazorWpfApp.Components;
namespace Tests;
public class CounterCSharpTests : TestContext
{
[Fact]
public void CounterStartsAtZero()
{
// Arrange
var cut = RenderComponent<Counter>();
// Assert that content of the paragraph shows counter at zero
cut.Find("p").MarkupMatches("<p>Current count: 0</p>");
}
// ... 省略
}
すると、ひとまずWPFアプリ側のCounterコンポーネントが認識されるようになるはずです。
元からあるCounterCSharpTests.csとCounterRazorTests.razorを開いてみましたが、WPFアプリ側の名前空間を参照できない現象が起こりました。
何度かVSCodeを開きなおしてみたら直ったので拡張機能の問題かなと思います。
単体試験を書いてみる
既存のCounterCSharpTests.csとCounterRazorTests.razorはもう利用しないので削除します。
xUnitでロジックの試験を書いてみます(テストコード自体は適当です)。
using System;
using System.IO;
using ClosedXML.Excel;
using BlazorWpfApp.Helpers;
namespace BlazorWpfApp.Tests.Helpers
{
public class ExcelHelperTests
{
[Fact]
public void CreateFile_Should_CreateExcelFile_WithExpectedContent()
{
// Arrange
var filePath = Path.Combine(Path.GetTempPath(), $"test_create_{Guid.NewGuid()}.xlsx");
// Act
ExcelHelper.CreateFile(filePath);
using var workbook = new XLWorkbook(filePath);
var ws = workbook.Worksheet("Sheet1");
// Assert
Assert.True(File.Exists(filePath), "ファイルが作成されていません。");
Assert.Equal("名前", ws.Cell("A1").GetValue<string>());
Assert.Equal("点数", ws.Cell("B1").GetValue<string>());
Assert.Equal("田中", ws.Cell("A2").GetValue<string>());
Assert.Equal(90, ws.Cell("B2").GetValue<int>());
Assert.Equal("佐藤", ws.Cell("A3").GetValue<string>());
Assert.Equal(85, ws.Cell("B3").GetValue<int>());
// Clean up
File.Delete(filePath);
}
}
}
エラーが出ることもなく実装までできました!
bUnitではBlazorコンポーネントの挙動や状態をテストできるので書いてみます。
@inherits TestContext
@code {
[Fact]
public void InitialCount_ShouldBeZero()
{
// Arrange
IRenderedComponent<Counter>? cut = RenderComponent<Counter>();
// Act
string? countText = cut.Find(cssSelector: "span.badge").TextContent;
// Assert
Assert.Equal("0", countText);
}
}
こちらも特に問題なく実装できました。
ルートディレクトリでコマンドラインからテストを実行してみます。
デフォルトの状態だとVSCodeにはテストエクスプローラーが無いですが、ひとまず試験結果の確認までできました!
![]()
ただ手放しで喜べるわけでもなく、試験に失敗した際には長々とスタックトレースが表示されるため、試験項目と結果一覧の表示には拡張機能をインストールした方が良さそうです。
結合試験も書いてみる
ロジックとUIの単体試験までできたので、最後に次のような試験もやってみます。
- DIコンテナに登録されたExcel操作のサービスをコンポーネントで呼び出す
- エクセル作成ボタンを押下
- "✅ Excelファイルを出力しました!"がUI上に表示されることを確認
Web APIの呼び出しもあるようなアプリだと、DIコンテナに各種サービスが登録されることになると思いますので、疑似的にそういったパターンを再現します(ExcelServiceクラスがそれだと思ってください)。
途中の準備段階を書くととても長くなってしまうので、変更点一覧と最終的なテストコードだけ示します。
- 変更点
-
ExcelHelperクラスをサービスに変更しDIコンテナへ登録する - 上記サービスを
Excel.razorコンポーネントで参照 -
bUnitのサービスの注入機能を利用する
-
@using BlazorWpfApp.Services
@inherits TestContext
@code {
[Fact]
public void ExportButton_ShouldDisplaySuccessMessage_WhenClicked()
{
// Arrange
Services.AddSingleton<IExcelService>(sp => new ExcelService("output.xlsx"));
// コンポーネントをレンダリング
IRenderedComponent<Excel>? cut = RenderComponent<Excel>();
// Act
cut.Find(cssSelector: "button").Click(); // "Export files"ボタンをクリック
// Assert
cut.Markup.Contains("✅ Excelファイルを出力しました!");
}
}
最後にテスト実施してみます。
dotnet test
ということで、こちらも問題なく実施できました! ![]()
まとめ/感想
-
いい感じな点
- ロジックは通常通り試験可能
- デスクトップアプリのUIをWebアプリの技術スタックで試験できる(すごい!)
-
bUnitとxUnitが違和感なく融合しているので、xUnit使ったことがある方なら比較的学習コストは低そう
-
若干の課題感
- 最初の結論に書いた通り、E2E試験はまだ簡単には行えなさそう…
- テストエクスプローラーについては拡張機能頼みになってしまう
- コマンドラインからテスト実施できるものの、エラーが出た際の視認性が良くない
とはいえ、シンプルで小さなアプリの開発に必要な要素を一通り確認できたかなと思います。
また分からない箇所が出たら適宜検証 → 記事にまとめたいと思います。

