Nuxt のモジュールをテストする

以下のような構成のモジュールをつくったとして、テストをどのように書くことができるか整理します。

.
├── README.md
├── __tests__
│   ├── fixtures
│   │   ├── nuxt.config.js
│   │   └── pages
│   │       └── index.vue
│   └── index.js
├── jest.config.js
├── lib
│   ├── module.js
│   └── plugin.template.js
├── package-lock.json
└── package.json

モジュールは「Google Analytics を設定するプラグインを提供するモジュール」として話を進めます。

モジュール本体

実際の機能を提供しているファイルは lib/module.js, lib/plugin.template.js です。

// module.js
const path = require('path');

module.exports = async function Module(moduleOptions) {
  const options = {
    // Default values
    id: undefined,
      
    ...this.options['ga'], // options via nuxt.config.js
    ...moduleOptions,
  };

  this.addPlugin({
    src: path.resolve(__dirname, 'plugin.template.js'),
    fileName: 'ga.js',
    options,
    ssr: false,
  });
};
// plugin.template.js
export default ({ app }) => {
  const moduleOptions = <%= serialize(options) %>;
  if (!id) {
    return;
  }
  // Load analytics.js
  (function(i, s, o, g, r, a, m) {
    // ...
  })(
    window,
    document,
    'script',
    'https://www.google-analytics.com/analytics.js',
    'ga'
  );

  // Create a tracker
  ga('create', id, 'auto');

  app.router.afterEach(to => {
    // Update the tracker
    ga('set', 'page', to.fullPath);
    // Send a pageview hit
    ga('send', 'pageview');
  });
};

詳細な説明は省きますが、モジュールオプションか nuxt.config.js のトップレベルのオプションを利用して GA のトラッキング ID を設定することが可能です。トラッキング ID が設定されない場合は GA のスクリプトがロードされないようになっています。

テストの方針

Nuxt のモジュールは Nuxt がビルドされる時に呼び出されます。そのため、モジュール関連のテストをするには Nuxt をプログラムから利用する必要があります(*)。

今回はテスト用に nuxt.config.jspages/index.vue を用意し、テストコード内では Nuxt インスタンスを作成 → ビルド → サーバ起動 を行い、実行結果が期待したものになるか確認をします。

(*) すべてのケースでこのように Nuxt をプログラムから利用する必要はないと思っています。自分が作成したロジックだけをテストするのであれば、別の方法も有効かもしれません。今回のテストも、プラグインのテストだけなら plugin.template.js を lodash template でコンパイルするという選択もあります。Nuxt をビルドするテストは時間がかかるという点も忘れない方がいいです。

(WIP) テストファイル

(記事は途中ですが、以下コードは問題なく動作します。現段階だと説明がわかりにくいだけです。)

// __tests__/index.js
const { Nuxt, Builder } = require('nuxt');

process.env.PORT = process.env.PORT || 3000;

const config = require('./fixtures/nuxt.config');
const url = path => `http://localhost:${process.env.PORT}${path}`;

// Nuxt のビルドには時間がかかるので jest のデフォルトタイムを変更
// https://jestjs.io/docs/ja/jest-object#jestsettimeouttimeout
jest.setTimeout(60000);

describe('ga module', () => {
  let nuxt;
  let addTemplate;

  /**
   * テスト用に用意した `nuxt.config` を利用して Nuxt インスタンスを生成
   * ビルドが完了したらサーバを起動
   */
  const initNuxt = async config => {
    if (nuxt) {
      await clearNuxt();
    }

    // https://ja.nuxtjs.org/api/nuxt#nuxt-%E3%81%AE%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF
    nuxt = new Nuxt(config);
    addTemplate = nuxt.moduleContainer.addTemplate = jest.fn(
      nuxt.moduleContainer.addTemplate
    );
    await new Builder(nuxt).build();

    // https://github.com/nuxt/nuxt.js/blob/8786ff731767425bb0c1c72af6d8a8895155acf5/packages/server/src/server.js#L225
    await nuxt.server.listen(process.env.PORT);
  };

  /**
   * Nuxt サーバーをクローズする
   */
  const clearNuxt = async () => {
    await nuxt.close();
  };

  /**
   * addTemplate (jest のモック関数) を利用してテンプレートオプションを取得
   */
  const getTemplateOptions = () => {
    const call = addTemplate.mock.calls.find(args =>
      args[0].src.includes('plugin.template.js')
    );
    return call && call[0] && call[0].options;
  };
 
  afterEach(async () => {
    await clearNuxt();
  });
 
  test('installs ga script and sets trackingId with Nuxt options', async () => {
    // トップレベルオプションに `ga` 設定を追加
    await initNuxt({
      ...config,
      ga: {
        id: 'WITH_NUXT_OPTIONS',
      },
    });

    // https://ja.nuxtjs.org/api/nuxt-render-and-get-window
    // > Nuxt.js アプリケーションの URL を渡して window を取得します。
    // 
    // `__tests__/fixtures/pages/index.vue` がレンダーされます
    const window = await nuxt.renderAndGetWindow(url('/'));

    expect(window.ga).toBeDefined();
    expect(getTemplateOptions().id).toBe('WITH_NUXT_OPTIONS');
  }); 
});
// __tests__/fixtures/nuxt.config.js
const { resolve } = require('path');

module.exports = {
  srcDir: __dirname,
  
  // https://ja.nuxtjs.org/api/configuration-dev
  // 
  // https://github.com/nuxt/nuxt.js/blob/8786ff731767425bb0c1c72af6d8a8895155acf5/packages/builder/src/builder.js#L57
  // dev が true だと監視状態になります。無効にしておいたほうがよさそうです。
  dev: false,

  // @@ はエイリアスで `rootDir` を指しています。
  // https://ja.nuxtjs.org/guide/directory-structure/#%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9-%E5%88%A5%E5%90%8D-
  modules: ['@@'],
};

参考