1
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?

[ESModule]fetch通信しているコードをJestを使ってテストコードを作成する

Last updated at Posted at 2025-12-26

Jestは、初めてなので備忘録として記録しておきます。

下記の記事の続編となります。

例えば、下記のようなJSファイルがあるとします。

sample.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.ネットワーク → 行かせない

これらの情報から、テストコードの全体像を大まかに作っていきます。

sample.test.js
//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関数の振る舞いを自分で作ることです。
こうすることで、テストが早くかつ安定します。

仮に本物のサーバにわざわざ通信してテストしようとすると...
●ネットワークにアクセスしないとテストが動かない
●サーバーのデータが変わるとテストが失敗する可能性がある
つまり 安定したテストにならない んです。
だから、仮のモックサーバの作成が必要なのです。

なので、テストコードの全体像はこう書きます。

sample.test.js
//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操作エラー処理などのテストができません。

sample.test.js
import { loadFruits } from '../src/fruits';
jest.mock('../src/fruits', () => ({
  loadFruits: jest.fn()
}));

なので、fetchをグローバルモック化することで⇩本物の関数の中身(DOM操作や then チェーン)もそのまま実行できるし、成功パターンやエラーパターンを自由にテストできます。

sample.test.js
global.fetch = jest.fn();

手順② fetchの呼び出しをモック化する

fetchは1回の呼び出しで、fetchの引数は「'/api/fruits'」であるかのテストをします。
なので、テストコードの全体像はこう追加します。

sample.test.js
//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
sample.test.js
expect(fetch).toHaveBeenCalledTimes(1);

👉 何回呼ばれたか をチェック
バグで2回呼ばれたらテストが落ちます。

toHaveBeenCalledWith
sample.test.js
expect(fetch).toHaveBeenCalledWith('/api/fruits');

👉 引数が一致しているか をチェック
/api/fruit(s抜け)みたいなミスを防げます。

応用:オプション付きfetchの場合

もし将来こうなったら👇

sample.js
fetch('/api/fruits', { method: 'GET' })

テストはこう書けます:

sample.test.js
expect(fetch).toHaveBeenCalledWith(
  '/api/fruits',
  { method: 'GET' }
);

手順③ Controllerから取得した値のテスト

モックサーバから取得した値とその数の確認をします。
そのため、テストコードは気のように追記します。

sample.test.js
//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');
    });
});

手順④ サーバ通信失敗時のテストケース

sample.test.js
//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);
  });
});

1
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
1
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?