「jQueryのテストって、どうやって書けばいいの?」
「テスト駆動開発(TDD)って何から始めればいいんだろう?」
「モック化やスパイって、実際どう使うの?」
前回はjQueryプラグインのリファクタリングについて学びました。今回は、モダンなテストフレームワークであるJestを使用して、テストしやすいコードへの移行方法を解説します。
テストしにくいコードの例
まず、テストが難しい従来のコードを見てみましょう:
JS
// jQuery版
function UserManager() {
this.loadUsers = function() {
$.ajax({
url: '/api/users',
success: function(users) {
users.forEach(function(user) {
$('#userList').append(
'<li>' + user.name + '</li>'
);
});
},
error: function(xhr) {
$('#error').text('Failed to load users');
}
});
};
}
このコードの問題点:
- 外部APIへの依存
- DOMへの直接操作
- グローバルなjQuery依存
- 非同期処理のテストが困難
テスト可能なコードへのリファクタリング
JS
// モダンな実装
class UserManager {
constructor(apiClient, renderer) {
this.apiClient = apiClient;
this.renderer = renderer;
}
async loadUsers() {
try {
const users = await this.apiClient.getUsers();
this.renderer.renderUsers(users);
} catch (error) {
this.renderer.showError('Failed to load users');
}
}
}
// 依存するクラスの実装
class APIClient {
async getUsers() {
const response = await fetch('/api/users');
return response.json();
}
}
class UserRenderer {
constructor(container) {
this.container = container;
}
renderUsers(users) {
const html = users.map(user =>
`<li>${user.name}</li>`
).join('');
this.container.innerHTML = html;
}
showError(message) {
this.container.innerHTML = `<div class="error">${message}</div>`;
}
}
改善点:
- 依存性の注入
- 関心の分離
- テスト可能なインターフェース
- 非同期処理の明確化
Jestでのテスト実装
JS
// user-manager.test.js
describe('UserManager', () => {
// モックの準備
let mockApiClient;
let mockRenderer;
let userManager;
beforeEach(() => {
mockApiClient = {
getUsers: jest.fn()
};
mockRenderer = {
renderUsers: jest.fn(),
showError: jest.fn()
};
userManager = new UserManager(mockApiClient, mockRenderer);
});
test('正常系: ユーザー一覧の取得と描画', async () => {
// テストデータの準備
const mockUsers = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
mockApiClient.getUsers.mockResolvedValue(mockUsers);
// テスト対象の実行
await userManager.loadUsers();
// 検証
expect(mockApiClient.getUsers).toHaveBeenCalled();
expect(mockRenderer.renderUsers)
.toHaveBeenCalledWith(mockUsers);
});
test('異常系: APIエラーの処理', async () => {
// エラーのモック
mockApiClient.getUsers.mockRejectedValue(new Error('API Error'));
// テスト対象の実行
await userManager.loadUsers();
// 検証
expect(mockRenderer.showError)
.toHaveBeenCalledWith('Failed to load users');
});
});
テストのポイント:
- テストの準備(Arrange)、実行(Act)、検証(Assert)の明確な分離
- モックを使用した依存性の制御
- 非同期処理のテスト方法
- エラーケースのテスト
APIクライアントのテスト
JS
// api-client.test.js
describe('APIClient', () => {
beforeEach(() => {
// fetchのモック化
global.fetch = jest.fn();
});
test('getUsers メソッドのテスト', async () => {
// モックレスポンスの準備
const mockUsers = [{ id: 1, name: 'John' }];
global.fetch.mockResolvedValue({
json: () => Promise.resolve(mockUsers)
});
const client = new APIClient();
const result = await client.getUsers();
expect(fetch).toHaveBeenCalledWith('/api/users');
expect(result).toEqual(mockUsers);
});
});
レンダラーのテスト
JS
// user-renderer.test.js
describe('UserRenderer', () => {
let container;
let renderer;
beforeEach(() => {
// DOMの準備
document.body.innerHTML = '<div id="container"></div>';
container = document.querySelector('#container');
renderer = new UserRenderer(container);
});
test('ユーザーリストの描画', () => {
const users = [
{ name: 'John' },
{ name: 'Jane' }
];
renderer.renderUsers(users);
expect(container.innerHTML).toContain('John');
expect(container.innerHTML).toContain('Jane');
});
test('エラーメッセージの表示', () => {
renderer.showError('Test Error');
expect(container.innerHTML)
.toContain('Test Error');
expect(container.querySelector('.error'))
.not.toBeNull();
});
});
テストカバレッジの確認
JS
// jest.config.js
module.exports = {
collectCoverage: true,
coverageReporters: ['text', 'html'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
カバレッジレポートの見方:
- Statements: 実行された文の割合
- Branches: 条件分岐の網羅率
- Functions: テストされた関数の割合
- Lines: 実行されたコードの行数割合
スナップショットテスト
JS
// user-renderer.test.js
describe('UserRenderer Snapshots', () => {
test('ユーザーリストのスナップショット', () => {
const users = [
{ name: 'John' },
{ name: 'Jane' }
];
const renderer = new UserRenderer(document.createElement('div'));
renderer.renderUsers(users);
expect(renderer.container.innerHTML)
.toMatchSnapshot();
});
});
スナップショットテストの利点:
- UI変更の検出が容易
- 意図しない変更の防止
- ビジュアルレグレッションの防止
まとめ
テスト可能なコードへの移行で得られるメリット:
- バグの早期発見
- リファクタリングの安全性向上
- 設計の改善
- ドキュメントとしての役割
次回は「デバッグテクニック – 開発者ツールを使いこなす」について解説し、より効率的なデバッグ方法を学んでいきましょう。