•8 min read

実際のブラウザでVueコンポーネントをテストする:アーキテクチャ、リアクティビティ、QUnit

実際のブラウザでVueコンポーネントをテストする:アーキテクチャ、リアクティビティ、QUnit

現代のほとんどのフロントエンドテストワークフローは、jsdomやhappy-domのようなシミュレートされたDOM環境を使用してNodeJS内で実行されます。シミュレートされたDOMは高速ですが、根本的な盲点があります。真のCSSレイアウトジオメトリ(getBoundingClientRect)を計算せず、SVGフィルターグラフィックをレンダリングせず、実際のブラウザのペイントとイベントバブリングの動作を正確にシミュレートできません。

キャンバスグラフィック、リッチテキストエディタ、動的なドラッグアンドドロップテーブル、高忠実度アニメーションなどの複雑なインタラクティブUIコンポーネントをテストする場合、実際のブラウザインスタンス内で直接テストを実行することで、忠実度のギャップを解消できます。

このガイドでは、QUnitを使用したVue 3コンポーネント向けの軽量で高性能なインブラウザテストランナーの構築、nextTick()による非同期リアクティビティの管理、そしてこのアーキテクチャと最新のヘッドレスブラウザランナーとの比較について説明します。


Audio Briefing
0:00 / 0:00

なぜ実際のブラウザ内でテストするのか?

JSDOMのようなシミュレートされたNodeJS DOM環境は、ブラウザAPIの近似です。シミュレートされた環境が本番環境で失敗する例を以下に示します。

  1. レイアウトとジオメトリ: jsdomは、すべての要素の寸法(offsetHeight、clientWidth、getBoundingClientRect)に対して0を返します。コンポーネントがコンテナサイズに基づいてレイアウトを調整する場合、JSDOMでのテストは役に立ちません。
  2. フォーカスと選択: ブラウザ固有のフォーカスリング、選択範囲、キーボードタブインデックスは、実際のブラウザレンダリングエンジン(Blink vs WebKit)間で動作が異なることがよくあります。
  3. 即時ビジュアルデバッグ: インブラウザテストが失敗した場合、実際のコンポーネントは画面にレンダリングされたままになります。Chrome DevToolsを開き、計算されたCSSスタイルを検査し、実行中のコンポーネントに直接ブレークポイントを設定できます。

Advertisement

インブラウザテストランナーのセットアップ

QUnitは、最も長く稼働しており、最も信頼性の高いJavaScriptテストフレームワークの1つです。クリーンなブラウザインターフェースと、テスト実行後に自動的にリセットされる分離された#qunit-fixture DOMコンテナが付属しています。

1. テストハーネスHTML(test-runner.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Vue 3 Browser Component Tests</title>
  <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.20.0.css">
</head>
<body>
  <div id="qunit"></div>
  <div id="qunit-fixture"></div>

  <!-- Load QUnit, Vue 3, and Bundled Components -->
  <script src="https://code.jquery.com/qunit/qunit-2.20.0.js"></script>
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
  <script src="./dist/components.bundle.js"></script>
  <script src="./tests/components.test.js"></script>
</body>
</html>

2. マウントヘルパー

コンポーネントを#qunit-fixtureにバインドし、ティアダウンハンドルを返す分離されたマウントユーティリティを作成します。

// test-helpers.js
function mountVueComponent(Component, props = {}, initialData = {}) {
  const fixture = document.getElementById('qunit-fixture');
  const container = document.createElement('div');
  fixture.appendChild(container);

  const app = Vue.createApp({
    render() {
      return Vue.h(Component, {
        ...props,
        ref: 'componentInstance',
      });
    },
    data() {
      return initialData;
    }
  });

  const vm = app.mount(container);

  return {
    app,
    container,
    vm: vm.$refs.componentInstance,
    // Helper to query elements within this component's DOM
    find: (selector) => container.querySelector(selector),
    findAll: (selector) => Array.from(container.querySelectorAll(selector)),
    // Teardown
    unmount: () => app.unmount(),
  };
}

Vue 3のリアクティビティのテスト: nextTick()の要件

VueはDOMを非同期に更新します。コンポーネントのリアクティブな状態が変更されても、VueはすぐにDOMを変更しません。代わりに、冗長なレイアウト計算を避けるために、内部のマイクロタスクキューで更新をバッファリングします。

状態変更直後にDOMコンテンツをアサートするテストは、DOMがまだフラッシュされていないため失敗します。

// ❌ FAILS: DOM has not flushed updates yet
component.count++;
assert.equal(element.textContent, 'Count: 1'); // Fails! Text is still 'Count: 0'

正しいパターン: await Vue.nextTick()

常にVueのリアクティビティキューが完了するのを待ちます。

// components.test.js
QUnit.module('CounterComponent Tests', (hooks) => {
  let mounted;

  hooks.afterEach(() => {
    if (mounted) mounted.unmount();
  });

  QUnit.test('increments counter on button click', async (assert) => {
    mounted = mountVueComponent(window.CounterComponent, { initialCount: 5 });
    
    const button = mounted.find('button.increment');
    const display = mounted.find('span.count');

    assert.equal(display.textContent.trim(), '5', 'Initial count renders correctly');

    // Simulate real user click event
    button.dispatchEvent(new MouseEvent('click', { bubbles: true }));

    // Await reactivity batch
    await Vue.nextTick();

    assert.equal(display.textContent.trim(), '6', 'DOM updates after click event');
  });
});

非同期フォームと入力のテスト

ユーザーの入力をシミュレートするには、inputとchangeの両方のイベントをトリガーして、Vueのv-modelディレクティブが内部状態を同期するようにする必要があります。

QUnit.module('FeedbackForm Tests', (hooks) => {
  let mounted;

  hooks.afterEach(() => {
    if (mounted) mounted.unmount();
  });

  QUnit.test('submits valid feedback and displays confirmation', async (assert) => {
    mounted = mountVueComponent(window.FeedbackForm);

    const input = mounted.find('input[name="email"]');
    const textarea = mounted.find('textarea[name="message"]');
    const form = mounted.find('form');

    // Populate form fields
    input.value = 'developer@locionic.com';
    input.dispatchEvent(new Event('input', { bubbles: true }));

    textarea.value = 'The browser test runner is incredibly fast!';
    textarea.dispatchEvent(new Event('input', { bubbles: true }));

    await Vue.nextTick();

    // Trigger form submit
    form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));

    // Wait for async submission simulation
    await new Promise((resolve) => setTimeout(resolve, 50));
    await Vue.nextTick();

    const successMessage = mounted.find('.alert-success');
    assert.ok(successMessage, 'Success message appears in the DOM');
    assert.includes(successMessage.textContent, 'Thank you', 'Correct confirmation text rendered');
  });
});

Advertisement

アーキテクチャ比較: インブラウザQUnit vs 最新のVitestブラウザモード

近年、フロントエンドエコシステムではVitestブラウザモード(Playwrightを介して実際のChromium/Firefox/WebKitインスタンスを起動する)が開発されました。インブラウザテストの比較は以下の通りです。

機能軽量QUnitブラウザハーネスVitestブラウザモードJSDOM (NodeJS)
実行速度サブミリ秒(即時)高速(ファイルあたり約100ms)非常に高速
リアルブラウザレンダリングはい(リアルDOMとCSS)はい(Chromium/WebKit)いいえ(フェイクDOM)
レイアウトジオメトリ計算100%正確100%正確サポートなし(0px)
セットアップの複雑さインストール不要(静的HTML)中(Vite設定)低(Nodeランナー)
CI自動化Headless Chromeが必要Playwright CLIに組み込みネイティブNode CLI

よくある質問

なぜPlaywrightをすべてに利用しないのですか?

Playwrightは、複数のページ、サーバーAPI、データベース移行にまたがる完全なエンドツーエンドのユーザー体験のために設計されています。強力ですが、E2Eテストは遅く、完全なバックエンドサーバーを起動する必要があります。インブラウザコンポーネントテストは、単体テストの速度と粒度で、実際のブラウザDOMの精度を提供します。

ブラウザテストでHTTP APIリクエストをモックするにはどうすればよいですか?

テストハーネス内でグローバルなwindow.fetch関数をスタブするか、Mock Service Worker(MSW)を使用できます。MSWは、ブラウザのネットワーク層でアウトバウンドHTTP呼び出しを傍受するネイティブブラウザService Workerをインストールし、コンポーネントコードを変更せずに決定論的なJSONフィクスチャを返します。

#qunit-fixtureはVueのイベントリスナーをクリーンアップしますか?

#qunit-fixtureコンテナはDOM要素をクリーンアップしますが、コンポーネントがwindowまたはdocumentにリスナーをアタッチしている場合(例: keydownまたはresizeをリッスンしている場合)、テスト間のメモリリークを避けるために、コンポーネントのunmount()メソッドがそれらを明示的にデタッチするようにする必要があります。


こちらもおすすめです

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement