【要点】
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: 基本を試す
- シンプルなコンポーネント(テキスト表示のみ)のテストを書く
-
npm run test:unitでテストを実行 - 成功したら、わざとテストを失敗させてエラーメッセージを確認
ステップ2: 実践的なテスト
- ボタンクリックのテストを追加
- プロパティの変更をテスト
- 条件分岐のあるロジックをテスト
ステップ3: 発展
- Apexモックを使ったテスト
- カスタムイベントのテスト
- テストカバレッジ80%以上を目指す
【次のステップ】
- @salesforce/sfdx-lwc-jest の公式ドキュメント確認
- テストカバレッジの確認と改善
- CI/CDパイプラインにテスト実行を組み込む
- テストデータビルダーパターンの学習