0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LWCのJestテスト作成ガイド

Last updated at Posted at 2026-01-08

【要点】

LWCのJestテストは、コンポーネントの動作を自動検証するためのテストコードです。基本的な流れは「コンポーネントを作成 → DOM操作をシミュレート → 結果を検証」の3ステップです。

【Jestテストの基本概念】

Jestとは

  • LWC専用のJavaScriptテストフレームワーク
  • コンポーネントのロジックやDOM操作を検証
  • Salesforce CLIに標準で組み込まれている

テストファイルの配置

force-app/main/default/lwc/myComponent/
├── myComponent.html
├── myComponent.js
├── myComponent.js-meta.xml
└── __tests__/
    └── myComponent.test.js  ← ここにテストを書く

【セットアップ手順】

1. Salesforce CLIでプロジェクト作成済みの場合

# プロジェクトルートで実行
npm install

# テスト実行
npm run test:unit

2. 既存プロジェクトへの追加

# package.jsonのscriptsセクションに追加
"scripts": {
  "test:unit": "sfdx-lwc-jest",
  "test:unit:watch": "sfdx-lwc-jest --watch",
  "test:unit:debug": "sfdx-lwc-jest --debug"
}

【基本的なテストの書き方】

テンプレート構造

// __tests__/myComponent.test.js

import { createElement } from 'lwc';
import MyComponent from 'c/myComponent';

describe('c-my-component', () => {
    // 各テスト後にDOMをクリーンアップ
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    // テストケース1: コンポーネントが正しく作成される
    it('正常にレンダリングされること', () => {
        // コンポーネント作成
        const element = createElement('c-my-component', {
            is: MyComponent
        });
        document.body.appendChild(element);

        // 検証: コンポーネントが存在する
        expect(element).not.toBeNull();
    });
});

【実践的なテストパターン】

パターン1: プロパティの設定と表示確認

// myComponent.js(テスト対象)
import { LightningElement, api } from 'lwc';

export default class MyComponent extends LightningElement {
    @api userName = 'ゲスト';
}
<!-- myComponent.html -->
<template>
    <div class="greeting">こんにちは、{userName}さん!</div>
</template>
// __tests__/myComponent.test.js
import { createElement } from 'lwc';
import MyComponent from 'c/myComponent';

describe('c-my-component プロパティテスト', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('userNameプロパティが正しく表示される', () => {
        // コンポーネント作成
        const element = createElement('c-my-component', {
            is: MyComponent
        });
        
        // プロパティ設定
        element.userName = '太郎';
        document.body.appendChild(element);

        // DOM更新を待つ
        return Promise.resolve().then(() => {
            // 表示内容を取得
            const greetingDiv = element.shadowRoot.querySelector('.greeting');
            
            // 検証: 正しいテキストが表示されている
            expect(greetingDiv.textContent).toBe('こんにちは、太郎さん!');
        });
    });
});

パターン2: ボタンクリックのイベント処理

// myComponent.js
import { LightningElement, track } from 'lwc';

export default class MyComponent extends LightningElement {
    @track count = 0;

    handleClick() {
        this.count++;
    }
}
<!-- myComponent.html -->
<template>
    <div>
        <p class="count">カウント: {count}</p>
        <button onclick={handleClick}>増やす</button>
    </div>
</template>
// __tests__/myComponent.test.js
describe('c-my-component ボタンクリックテスト', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('ボタンクリックでカウントが増える', () => {
        // コンポーネント作成
        const element = createElement('c-my-component', {
            is: MyComponent
        });
        document.body.appendChild(element);

        // 初期状態の確認
        const countDisplay = element.shadowRoot.querySelector('.count');
        expect(countDisplay.textContent).toBe('カウント: 0');

        // ボタンを取得してクリック
        const button = element.shadowRoot.querySelector('button');
        button.click();

        // DOM更新を待って検証
        return Promise.resolve().then(() => {
            expect(countDisplay.textContent).toBe('カウント: 1');
        });
    });

    it('複数回クリックでカウントが正しく増える', () => {
        const element = createElement('c-my-component', {
            is: MyComponent
        });
        document.body.appendChild(element);

        const button = element.shadowRoot.querySelector('button');
        
        // 3回クリック
        button.click();
        button.click();
        button.click();

        return Promise.resolve().then(() => {
            const countDisplay = element.shadowRoot.querySelector('.count');
            expect(countDisplay.textContent).toBe('カウント: 3');
        });
    });
});

パターン3: Apex呼び出しのモック

// myComponent.js
import { LightningElement, wire } from 'lwc';
import getAccountList from '@salesforce/apex/AccountController.getAccountList';

export default class MyComponent extends LightningElement {
    @wire(getAccountList)
    accounts;
}
// __tests__/myComponent.test.js
import { createElement } from 'lwc';
import MyComponent from 'c/myComponent';
import getAccountList from '@salesforce/apex/AccountController.getAccountList';

// Apexメソッドをモック化
jest.mock(
    '@salesforce/apex/AccountController.getAccountList',
    () => {
        return {
            default: jest.fn()
        };
    },
    { virtual: true }
);

describe('c-my-component Apexモックテスト', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
        // モックをリセット
        jest.clearAllMocks();
    });

    it('Apexから取得したデータが表示される', () => {
        // モックデータを準備
        const mockAccounts = [
            { Id: '001', Name: 'テスト取引先A' },
            { Id: '002', Name: 'テスト取引先B' }
        ];

        // Apexメソッドのモック設定
        getAccountList.mockResolvedValue(mockAccounts);

        // コンポーネント作成
        const element = createElement('c-my-component', {
            is: MyComponent
        });
        document.body.appendChild(element);

        // Promiseの解決を待つ
        return Promise.resolve().then(() => {
            // データが正しく設定されているか検証
            expect(element.accounts.data).toEqual(mockAccounts);
        });
    });

    it('Apexエラー時の処理', () => {
        // エラーをモック
        const mockError = { body: { message: 'エラーが発生しました' } };
        getAccountList.mockRejectedValue(mockError);

        const element = createElement('c-my-component', {
            is: MyComponent
        });
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            // エラーが正しく処理されているか検証
            expect(element.accounts.error).toBeTruthy();
        });
    });
});

【よく使うテストメソッド】

DOM操作・検証

// 要素の取得
const element = shadowRoot.querySelector('.my-class');
const elements = shadowRoot.querySelectorAll('li');

// 存在確認
expect(element).not.toBeNull();
expect(element).toBeTruthy();

// テキスト内容の確認
expect(element.textContent).toBe('期待する文字列');
expect(element.textContent).toContain('含まれる文字列');

// 属性の確認
expect(element.getAttribute('data-id')).toBe('123');
expect(element.classList.contains('active')).toBe(true);

// 配列・オブジェクトの確認
expect(data.length).toBe(3);
expect(result).toEqual({ name: 'test', value: 100 });

イベント発火

// クリックイベント
button.click();

// カスタムイベント
const event = new CustomEvent('myevent', {
    detail: { value: 'test' }
});
element.dispatchEvent(event);

// 入力イベント
inputField.value = 'new value';
inputField.dispatchEvent(new Event('change'));

【テスト実行コマンド】

# 全テストを実行
npm run test:unit

# 特定のファイルのみ実行
npm run test:unit -- myComponent

# ウォッチモード(ファイル変更を監視して自動実行)
npm run test:unit:watch

# カバレッジレポート生成
npm run test:unit -- --coverage

# デバッグモード
npm run test:unit:debug

【テスト作成の考え方・ベストプラクティス】

1. テストの粒度

  • 1つのテスト = 1つの動作検証
  • テストケース名は「何を検証するか」を明確に
// 良い例
it('ボタンクリックでカウントが1増える', () => { ... });
it('未入力時にエラーメッセージが表示される', () => { ... });

// 悪い例
it('テスト1', () => { ... });
it('動作確認', () => { ... });

2. AAA (Arrange-Act-Assert) パターン

it('テストケース', () => {
    // Arrange: テストの準備(データ、コンポーネント作成)
    const element = createElement('c-my-component', { is: MyComponent });
    element.value = 'test';
    document.body.appendChild(element);

    // Act: 実行(ボタンクリック、メソッド呼び出し)
    const button = element.shadowRoot.querySelector('button');
    button.click();

    // Assert: 検証(期待する結果と実際の結果を比較)
    return Promise.resolve().then(() => {
        expect(element.result).toBe('expected');
    });
});

3. テストすべき内容の優先順位

優先度高:

  • ユーザー操作に直結する機能(ボタンクリック、入力)
  • 条件分岐のロジック
  • データの表示・更新

優先度中:

  • エラーハンドリング
  • エッジケース(空データ、最大値など)

優先度低(またはテスト不要):

  • LWCフレームワーク自体の動作
  • 単純なgetter/setter

4. 非同期処理の扱い

// Promise.resolve()でDOM更新を待つ
it('非同期テスト', () => {
    // ... 処理 ...
    
    return Promise.resolve().then(() => {
        // DOM更新後の検証
        expect(element.result).toBe('updated');
    });
});

// async/awaitを使う方法(推奨)
it('非同期テスト(async版)', async () => {
    // ... 処理 ...
    
    await Promise.resolve();
    
    // DOM更新後の検証
    expect(element.result).toBe('updated');
});

【よくあるエラーと対処法】

エラー1: Cannot find module 'c/myComponent'

原因: テストファイルのインポートパスが間違っている
対処: コンポーネント名が正確か、c/で始まっているか確認

エラー2: Element is null

原因: DOM更新を待たずに要素を取得している
対処: Promise.resolve().then() で非同期処理を待つ

エラー3: shadowRoot is null

原因: コンポーネントがDOMに追加されていない
対処: document.body.appendChild(element) を実行してから要素を取得

【学習ステップ】

ステップ1: 基本を試す

  1. シンプルなコンポーネント(テキスト表示のみ)のテストを書く
  2. npm run test:unit でテストを実行
  3. 成功したら、わざとテストを失敗させてエラーメッセージを確認

ステップ2: 実践的なテスト

  1. ボタンクリックのテストを追加
  2. プロパティの変更をテスト
  3. 条件分岐のあるロジックをテスト

ステップ3: 発展

  1. Apexモックを使ったテスト
  2. カスタムイベントのテスト
  3. テストカバレッジ80%以上を目指す

【次のステップ】

  • @salesforce/sfdx-lwc-jest の公式ドキュメント確認
  • テストカバレッジの確認と改善
  • CI/CDパイプラインにテスト実行を組み込む
  • テストデータビルダーパターンの学習
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?