10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WPF + Blazor Hybrid で作ったアプリをテストしてみた

10
Last updated at Posted at 2025-11-27

:rocket: はじめに

WPF ( Blazor Hybrid )アプリをどこまで自動テストできるのか試してみました :writing_hand:

  • 想定読者

    • 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

:hammer_pick: ソリューションとプロジェクトの用意

まずは空のソリューションを作成します。

mkdir BlazorWpfAppWithTest
cd BlazorWpfAppWithTest
dotnet new sln -n BlazorWpfAppWithTest

WPFアプリを用意

WPFアプリも用意しますが、せっかくなので前回使ったアプリをそのまま流用します。

実務においては試験しやすいプロジェクト構成・フォルダ構成にしたほうが良いと思いますが、今回は簡単な検証用途なのでそのまま使います。

ソリューションに追加します。

dotnet sln BlazorWpfAppWithTest.sln add BlazorWpfApp

テストプロジェクトを用意

ロジックやUIテスト用にテストプロジェクトを作ります。bUnitの公式ドキュメントに記載されている方法に従って次の手順を踏みます。

  1. bUnitのテンプレートを用意
  2. ソリューションへ参照追加
  3. 細かい部分の修正
  4. WPFアプリへの参照を追加
  5. さらに細かい修正

手順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に変更します。

Tests.csproj
<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を都度追加しなくても良いようにしておきます。

_Imports.razor
@* WPFアプリ側で定義したBlazorコンポーネントの名前空間 *@ 
@using BlazorWpfApp.Components 

この状態でテンプレートには次の二つのファイルがあります。

  • Counter.razor
  • CounterCSharpTests.cs

CounterCSharpTests.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.csCounterRazorTests.razorを開いてみましたが、WPFアプリ側の名前空間を参照できない現象が起こりました。
何度かVSCodeを開きなおしてみたら直ったので拡張機能の問題かなと思います。

:point_up: 単体試験を書いてみる

既存のCounterCSharpTests.csCounterRazorTests.razorはもう利用しないので削除します。

xUnitでロジックの試験を書いてみます(テストコード自体は適当です)。

ExcelHelperTests.cs
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);
    }
}

こちらも特に問題なく実装できました。

ルートディレクトリでコマンドラインからテストを実行してみます。

qiita-wpf-test.png

デフォルトの状態だとVSCodeにはテストエクスプローラーが無いですが、ひとまず試験結果の確認までできました! :tada: :tada: :tada:

ただ手放しで喜べるわけでもなく、試験に失敗した際には長々とスタックトレースが表示されるため、試験項目と結果一覧の表示には拡張機能をインストールした方が良さそうです。

:v: 結合試験も書いてみる

ロジックとUIの単体試験までできたので、最後に次のような試験もやってみます。

  1. DIコンテナに登録されたExcel操作のサービスをコンポーネントで呼び出す
  2. エクセル作成ボタンを押下
  3. "✅ Excelファイルを出力しました!"がUI上に表示されることを確認

Web APIの呼び出しもあるようなアプリだと、DIコンテナに各種サービスが登録されることになると思いますので、疑似的にそういったパターンを再現します(ExcelServiceクラスがそれだと思ってください)。

途中の準備段階を書くととても長くなってしまうので、変更点一覧と最終的なテストコードだけ示します。

  • 変更点
ExcelTest.cs
@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

スクリーンショット 2025-11-11 171732.png

ということで、こちらも問題なく実施できました! :tada:

:bulb: まとめ/感想

  • いい感じな点

    • ロジックは通常通り試験可能
    • デスクトップアプリのUIをWebアプリの技術スタックで試験できる(すごい!)
    • bUnitxUnitが違和感なく融合しているので、xUnit使ったことがある方なら比較的学習コストは低そう
  • 若干の課題感

    • 最初の結論に書いた通り、E2E試験はまだ簡単には行えなさそう…
    • テストエクスプローラーについては拡張機能頼みになってしまう
    • コマンドラインからテスト実施できるものの、エラーが出た際の視認性が良くない:cry:

とはいえ、シンプルで小さなアプリの開発に必要な要素を一通り確認できたかなと思います。

また分からない箇所が出たら適宜検証 → 記事にまとめたいと思います。

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?