😺

AngleSharp で ASP.NET Core ウェブアプリケーションの統合テストをする

2025/01/01に公開

AngleSharp で ASP.NET Core ウェブアプリケーションの統合テストをする

ASP.NET Core には統合テストを支援するためのテスト用サーバーが用意されています。これは偽のサーバーインスタンスをテストインスタンスの中で起動し、それに接続するための偽の HttpClient を取得できます。

本物のサーバーインスタンスと違い、TCP でリッスンしません。その分偽のサーバーインスタンスは起動が速く、特にテスト駆動開発でもってこいです。

AngleSharp を使う理由

ASP.NET Core Web API であればウェブブラウザーはテストには不要です。ですが ASP.NET Core MVC などの UI を持つウェブアプリケーションでは Selenium などを利用してウェブブラウザーを起動してレンダリングさせないとテストが難しいです。さらには起動が高速な ASP.NET Core のテストサーバーが使えません。その分テストの実行に時間がかかります。

AngleSharp は HTML のパーサーで、.NET から扱える DOM インターフェイスを備えています。さらに JavaScript エンジンもオプションで用意されています。もうこうなるとウェブブラウザーのエミュレーターですよね。Google Chrome や Mozilla Firefox ではないのでそれらで動くのかどうかの統合テストはできませんが、代わりに高速です。

まあ、本物のブラウザーの代わりに jsdom を使うようなもんですね。

AngleSharp を統合テストに使う

テストサーバーの使い方や AngleSharp での HTML パースしてのテストは公式ドキュメントの ASP.NET Core での統合テスト にあります。このドキュメントに書かれていない、リンクをたどったり JavaScript もテストできるようにします。

リンクをたどるテスト

まずはリンクをたどるテストからです。

次のパッケージをテストプロジェクトに追加します。

コードは次のようになります。

// ASP.NET Core MVC のテストサーバーの起動と、それに接続する HttpClient の取得。
await using var factory = new WebApplicationFactory<Program>();
using var client = factory.CreateClient();

// AngleSharp の構成。
var config = Configuration.Default
    .With(new HttpClientRequester(client)) // テストサーバーにつなげます。
    .WithDefaultLoader()
    .WithDefaultCookies(); // Cookie を有効にします。
using var browser = BrowsingContext.New(config);

// 偽のブラウザーを開きます。
await browser.OpenAsync(new Uri(client.BaseAddress, "/").ToString());

var h1 = ((IHtmlElement)browser.Active.QuerySelector("""h1""")).TextContent;

Console.WriteLine(h1);

.With(new HttpClientRequester(client)) は AngleSharp.Io に含まれている HttpClientRequester クラスを利用しています。テストサーバーへ接続する HttpClient を AngleSharp で利用するよう設定しています。

多くの場合ログイン状態の維持が必要でしょうから、.WithDefaultCookies() で Cookie も有効にします。

オープンする URL はフルでないといけないので、new Uri(client.BaseAddress, "/").ToString() としてアドレスを作成しています。

オープンしたウェブサイトの DOM のドキュメントオブジェクトは browser.OpenAsync() の戻り値か browser.Active で取得できます。今回は後者を利用しています。

あとは AngleSharp が DOM インターフェイスを提供していますのでいつも通りに書いていくだけです。

ボタンやハイパーリンクのクリックは IHtmlElement.DoClick() を使います。

((IHtmlElement)browser.Active.QuerySelector("""a[href="/Privacy"]""")).DoClick();
await Task.Delay(1000); // 少し待つ。

var title = browser.Active.Title;

Console.WriteLine(title); // Privacy Policy ...

ページを遷移した直後はまだレンダリングされていないようで、雑に await Task.Delay(1000) を追加しています。

JavaScript も扱う

AngleSharp は標準では JavaScript が使えません。AngleSharp.Js パッケージをプロジェクトに追加してください。ただし、次の点に気を付けてください。

  • AngleSharp.Js の正式リリース版はかなり古く、(0.15.0 2021/06/13) ECMAScript 6 への対応が弱いです。dotnet package add --prerelease オプションを付けてベータ版を利用してください。(1.0.0 正式リリース後は不要です)
  • AngleSharp.Scripting.JavaScript というものもありますが、こちらはもうメンテナンスされていないようなので使わないでください。

ASP.NET Core のプロジェクトテンプレートには JavaScript がほぼ使われていないので、次のコードを Home/Index.cshtml に追加します。

<div>
    <span id="current"></span>
    <button id="counter">increment</button>
</div>

<script>
    let value = 0;
    const $counter = document.querySelector('#counter');
    const $current = document.querySelector('#current');
    $current.innerHTML = value;
    $counter.addEventListener('click', () => {
        value++;
        $current.innerHTML = value;
    });
</script>

これをテストします。

var config = Configuration.Default
    .WithJs() // JavaScript エンジンを有効にする。
    .With(new HttpClientRequester(client))
    .WithDefaultLoader()
    .WithDefaultCookies();
var browser = BrowsingContext.New(config);

await browser.OpenAsync(new Uri(client.BaseAddress, "/").ToString())
    .WaitUntilAvailable();

// インクリメントボタンをクリックする。
((IHtmlElement)browser.Active.QuerySelector("#counter")).DoClick();
// 完了するまで待つ。
browser.Active.WaitUntilAvailable();

// `1`
    Console.WriteLine(browser.Active.QuerySelector("#currwent").TextContent);

AngleSharp.Js に含まれる .WithJs() を利用して JavaScript エンジンを有効にしています。違いはこれくらいです。あとは AngleSharp の DOM インターフェイスを利用して操作します。

browser.Active.WaitUntilAvailable() で待機することができます。今回のケースでは書かなくても問題ありませんでした。

最後に

テスト駆動開発はテストの実施時間を大幅に短縮することで試行錯誤が容易になったことでできるようになっています。実ブラウザーを起動したテストはとても時間がかかり、テスト駆動開発に使うには苦しいです。

ASP.NET Core テストサーバー + AngleSharp はこれを解消します。

とはいえ、代償として AngleSharp でうまく動かない時のデバッグが大変だったり、テストは通ったけど実ブラウザー動かないとかがありそうですねぇ。

Discussion