💯

MapLibre GL JSをvitestでモックする

2024/05/31に公開

背景

コンポーネントがMapLibre GL JSを含む場合、以下のようなエラーが発生します

 FAIL  src/routes/map/page.test.ts [ src/routes/map/page.test.ts ]
TypeError: window.URL.createObjectURL is not a function
 ❯ define node_modules/maplibre-gl/dist/maplibre-gl.js:34:44
     32| 
     33|     if (typeof window !== 'undefined') {
     34|         maplibregl.setWorkerUrl(window.URL.createOb…
       |                                            ^
     35|     }
     36|

エラーを見るに、MapLibre GL JSがブラウザでのみ利用できるAPIに依存していることが原因でしょう。

対処法

https://stackoverflow.com/questions/48866088/testing-a-react-mapbox-gl-with-jsodom-and-jest/62681564#62681564

MapLibre GL JS自体をモックしてしまうのが良さそう。MapLibre GL JS自体の動作をテストする必要はないので(ライブラリ側でユニットテストされているし、実際の動きをテストしたい場合はE2Eテストでカバーすることになるだろう)。

サンプルコード

@testing-library/svelteを用いたコンポーネントのユニットテストを例としますが、ReactやVueでも同様の手法をとれます。

import { render } from '@testing-library/svelte';
import { describe, test, vi } from 'vitest';

import MapPage from './+page.svelte';

vi.mock('maplibre-gl', () => ({
    default: vi.fn(),
    Map: vi.fn().mockImplementation(() => ({
        setStyle: vi.fn(),
        once: vi.fn(),
        on: vi.fn(),
        addControl: vi.fn()
    })),
    GeolocateControl: vi.fn(),
    NavigationControl: vi.fn()
    // ほかにも大量の実装があるが、コンポーネントで呼び出されるAPIだけモックすればよい
}));

describe('Map', () => {
    test('render', async () => {
        render(MapPage);
    });
});

モックした関数がコールされることをテスト

モジュールをモックしたうえで、そのモック関数がコールされたかどうかチェックしたい場合、シンプルなケースでは以下のように書くだけで済みます。

import { Map } from 'maplibre-gl'; // importされるのはモック関数

// モック処理は省略

describe('MapPage', () => {
    test('render', async () => {
        render(MapPage);
        expect(Map).toHaveBeenCalledTimes(1);
    });
});

以下はSveltekitに限った内容かもしれません

ただし、MapLibre GL JSはDOMを操作する関数なので、副作用ーSvelteではonMountを利用してMapクラスを初期化することになります。しかし、特殊な設定をしないとonMountが発火せず、上記のテストコードは失敗します(Mapが呼ばれていないことになるため)。

onMountをテスト環境で動かすためには、少しテクいことをしなければならなそうです。

https://github.com/testing-library/svelte-testing-library/issues/222

以下のように、configを修正することで、render()onMount処理が走り(=Mapクラスが初期化され)、テストが成功します(どういう意味を持つ設定なのかはよくわからない)。

vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig, type Plugin } from 'vite';

// onMount処理のテストのためのプラグイン
const vitestBrowserConditionPlugin: Plugin = {
    name: 'vite-plugin-vitest-browser-condition',
    config({ resolve }) {
        if (process.env.VITEST) {
            resolve?.conditions?.unshift('browser');
        }
    }
};

export default defineConfig({
    plugins: [vitestBrowserConditionPlugin, sveltekit()],
    // @ts-ignore
    test: {
        include: ['src/**/*.{test,spec}.{js,ts}'],
        globals: true,
        environment: 'jsdom'
    }
});
MIERUNEのZennブログ

Discussion