Jestは、初めてなので備忘録として記録しておきます。
下記の記事の続編となります。
例えば、下記のようなJSファイルがあるとします。
export async function loadFruits(){
return fetch('/api/fruits')
.then(response => response.json())
.then(data =>{
const ul = document.getElementById('fruit-list');
data.forEach(item =>{
const li = document.createElement('li');
li.textContent = item;
ul.appendChild(li);
})
})
.catch(error =>{
console.log('エラー:', error);
})
}
これは 「fetch をモックして、DOM が正しく更新されるかを確認する」 典型パターンです。
テストでやりたいこと(整理)
loadFruits() は次のことをしています。
1./api/fruits にfetchする
2.レスポンスの JSON(配列)を受け取る
3.#fruit-list に<li>を追加する
👉 テストではこれを 全部本物でやらない のがポイントです。
1.fetch → モック
2.DOM → テスト用に用意
3.ネットワーク → 行かせない
これらの情報から、テストコードの全体像を大まかに作っていきます。
//ESModule版テストコード
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
import { loadFruits } from '../src/fruits';
// fetch をモック
describe('loadFruits', () => {
beforeEach(() => {
// DOMを初期化
// 共通の戻り値を設定
});
//
test('fetchが正しいURLで呼ばれる',async()=>{
// 非同期処理
// fetchが何回呼ばれたか をチェック
// fetchの引数が一致しているか をチェック
});
test('Controllerから取得した果物が画面に表示される', async () => {
// 非同期処理
// DOMのID値を取得
// モックサーバから返却された値の数を確認
// 返却された値の0番目から順に値が取り出せるか確認
});
});
手順① fetchをモック化する
テストする際は、fetchからサーバAPIは呼ばずに、仮のサーバを作成します。
つまり、「こういうデータが返ってきたことにする」という値を決め打ちしておくのです。
もっとわかりやすくいうと、「fetchをモックする」というのは、本物のサーバー通信をせずに、fetch関数の振る舞いを自分で作ることです。
こうすることで、テストが早くかつ安定します。
仮に本物のサーバにわざわざ通信してテストしようとすると...
●ネットワークにアクセスしないとテストが動かない
●サーバーのデータが変わるとテストが失敗する可能性がある
つまり 安定したテストにならない んです。
だから、仮のモックサーバの作成が必要なのです。
なので、テストコードの全体像はこう書きます。
//ESModule版テストコード
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
import { loadFruits } from '../src/fruits';
// fetch をモック
global.fetch = jest.fn();
describe('loadFruits', () => {
beforeEach(() => {
// DOMを初期化
document.body.innerHTML = `
<ul id="fruit-list"></ul>
`;
// 共通の戻り値を設定
fetch.mockResolvedValue({
json: jest.fn().mockResolvedValue(['Apple', 'Banana', 'Orange'])
});
});
//
test('fetchが正しいURLで呼ばれる',async()=>{
// 非同期処理
// fetchが何回呼ばれたか をチェック
// fetchの引数が一致しているか をチェック
});
test('Controllerから取得した果物が画面に表示される', async () => {
// 非同期処理
// DOMのID値を取得
// モックサーバから返却された値の数を確認
// 返却された値の0番目から順に値が取り出せるか確認
});
});
global.fetch = jest.fn();とすることで、loadFruits関数のDOM操作やエラー処理などのテストもしたいからです。
もし、loadFruits関数まるごとモック化⇩してしまうと、loadFruits関数のDOM操作やエラー処理などのテストができません。
import { loadFruits } from '../src/fruits';
jest.mock('../src/fruits', () => ({
loadFruits: jest.fn()
}));
なので、fetchをグローバルモック化することで⇩本物の関数の中身(DOM操作や then チェーン)もそのまま実行できるし、成功パターンやエラーパターンを自由にテストできます。
global.fetch = jest.fn();
手順② fetchの呼び出しをモック化する
fetchは1回の呼び出しで、fetchの引数は「'/api/fruits'」であるかのテストをします。
なので、テストコードの全体像はこう追加します。
//ESModule版テストコード
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
import { loadFruits } from '../src/fruits';
// fetch をモック
global.fetch = jest.fn();
describe('loadFruits', () => {
beforeEach(() => {
// DOMを初期化
document.body.innerHTML = `
<ul id="fruit-list"></ul>
`;
// 共通の戻り値を設定
fetch.mockResolvedValue({
json: jest.fn().mockResolvedValue(['Apple', 'Banana', 'Orange'])
});
});
//
test('fetchが正しいURLで呼ばれる',async()=>{
await loadFruits();
// 何回呼ばれたか をチェック
expect(fetch).toHaveBeenCalledTimes(1);
// 引数が一致しているか をチェック
expect(fetch).toHaveBeenCalledWith('/api/fruits');
});
test('Controllerから取得した果物が画面に表示される', async () => {
});
});
ここで使っている Jest のAPI解説
toHaveBeenCalledTimes
expect(fetch).toHaveBeenCalledTimes(1);
👉 何回呼ばれたか をチェック
バグで2回呼ばれたらテストが落ちます。
toHaveBeenCalledWith
expect(fetch).toHaveBeenCalledWith('/api/fruits');
👉 引数が一致しているか をチェック
/api/fruit(s抜け)みたいなミスを防げます。
応用:オプション付きfetchの場合
もし将来こうなったら👇
fetch('/api/fruits', { method: 'GET' })
テストはこう書けます:
expect(fetch).toHaveBeenCalledWith(
'/api/fruits',
{ method: 'GET' }
);
手順③ Controllerから取得した値のテスト
モックサーバから取得した値とその数の確認をします。
そのため、テストコードは気のように追記します。
//ESModule版テストコード
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
import { loadFruits } from '../src/fruits';
// fetch をモック
global.fetch = jest.fn();
describe('loadFruits', () => {
beforeEach(() => {
// DOMを初期化
document.body.innerHTML = `
<ul id="fruit-list"></ul>
`;
// 共通の戻り値を設定
fetch.mockResolvedValue({
json: jest.fn().mockResolvedValue(['Apple', 'Banana', 'Orange'])
});
});
//
test('fetchが正しいURLで呼ばれる',async()=>{
await loadFruits();
// 何回呼ばれたか をチェック
expect(fetch).toHaveBeenCalledTimes(1);
// 引数が一致しているか をチェック
expect(fetch).toHaveBeenCalledWith('/api/fruits');
});
test('Controllerから取得した果物が画面に表示される', async () => {
await loadFruits();
const liList = document.querySelectorAll('#fruit-list li');
expect(liList.length).toBe(3);
expect(liList[0].textContent).toBe('Apple');
expect(liList[1].textContent).toBe('Banana');
expect(liList[2].textContent).toBe('Orange');
});
});
手順④ サーバ通信失敗時のテストケース
//ESModule版テストコード
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
import { loadFruits } from '../src/fruits';
// fetch をモック
global.fetch = jest.fn();
describe('loadFruits', () => {
beforeEach(() => {
// DOMを初期化
document.body.innerHTML = `
<ul id="fruit-list"></ul>
`;
// 共通の戻り値を設定
fetch.mockResolvedValue({
json: jest.fn().mockResolvedValue(['Apple', 'Banana', 'Orange'])
});
});
//
test('fetchが正しいURLで呼ばれる',async()=>{
await loadFruits();
// 何回呼ばれたか をチェック
expect(fetch).toHaveBeenCalledTimes(1);
// 引数が一致しているか をチェック
expect(fetch).toHaveBeenCalledWith('/api/fruits');
});
test('Controllerから取得した果物が画面に表示される', async () => {
await loadFruits();
const liList = document.querySelectorAll('#fruit-list li');
expect(liList.length).toBe(3);
expect(liList[0].textContent).toBe('Apple');
expect(liList[1].textContent).toBe('Banana');
expect(liList[2].textContent).toBe('Orange');
});
});
describe('loadFruits エラーケース', () => {
let consoleSpy;
beforeEach(() => {
document.body.innerHTML = `<ul id="fruit-list"></ul>`;
consoleSpy = jest
.spyOn(console, 'log')
.mockImplementation(() => {});
});
test('fetch失敗時にエラーログが出力される', async () => {
const error = new Error('Network Error');
fetch.mockRejectedValue(error);
await loadFruits();
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(consoleSpy).toHaveBeenCalledWith('エラー:', error);
});
});