メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

【Nuxt 3移行】ユニットテストをNuxt 2から移行し、実行速度が4倍速くなった話

こんにちは。フロントエンドエンジニアの相澤 ( @ttt3pu ) です。

みなさま、Nuxt 2 から Nuxt 3 へのアップグレードは順調でしょうか。

メドピアでは、2023年末のVue 2のEOLへ向けて、
各プロダクトで積極的にNuxt 3へのアップグレードを進めています。

現在私の担当しているプロダクトでは、マイグレーション作業自体はほぼ完了しており、
残すはQAテストなどを行うのみと言う段階で、本番リリースまであと一歩というところまで進んでおります! 🎉

マイグレーションの事例も徐々に増え始めてきて、Nuxt 3のリリース当初よりも段々と移行はしやすくなってきましたが、
個人的に結構大変だったのがユニットテストのマイグレーション作業でした。

当記事では、マイグレーションに当たっての Tips と、ついでに Vitest を導入したことにより、
実行速度が 約 10分 -> 約2分30秒 ( 約 4倍 ! ) まで跳ね上がった話をご紹介します。

目次

対象プロジェクトのユニットテスト周りのライブラリ構成

まず、今回マイグレーション作業を行なったプロジェクトは、以下のようなライブラリ構成で構築されていました。

移行後は、以下のようなライブラリ構成となりました。

Jest は廃止し、 Vitest へ移行しています。

vue-test-utilsについては、Vue 3対応しているのがv2以降となるため、バージョンが上がっています。

今回肝となったのが「nuxt-vitest」の導入でした。
Nuxt 3 + Vitest の実行環境を用意してくれるライブラリです。
詳しい内容については後述させていただきます。

移行完了までのステップ

プロダクトの実装状況によって多少順番が変わってくるとは思いますが、
テストが全て通るまでに、以下のステップで作業を行いました。

  1. Jest から Vitest へ移行
  2. nuxt-vitestを導入
  3. vue-test-utils の v1 から v2 へのマイグレーション作業を実施
  4. 落ちているテストごとの個別修正
  5. Vitest の設定を調整し速度改善

ここから先は、それぞれのステップで行なった作業について、詳しくご紹介させていただきます。

Jest から Vitest への移行

一番最初のステップとして、nuxt-vitestを導入する前準備として、JestからVitestへの移行を行いました。

Jestとは互換性が高いので、導入はほとんど苦になりませんでした。

ざっくりとまとめると、以下の作業を行うだけで移行が完了しました。

  • jest.config.ts の各設定を vitest.cofig.ts へ移行する
  • jest メソッドを vi へ置換する
    • 例: jest.fn -> vi.fnjest.spyOn-> vi.spyOn など
  • npm scripts等にCLIの記述があれば変更する

詳細については、公式の Migration Guide をご参照ください!

vitest.dev

nuxt-vitest の導入

続いて nuxt-vitest を導入します。

nuxt-vitest は、Nuxt の開発コアメンバーである Daniel さんによって開発されている、Nuxt 3 + Vitest の実行環境を用意してくれるライブラリです。

github.com

導入することより、自分で実装しようとするとかなり複雑になってしまうような設定を、ある程度自動で行なってくれるようになります。
コアメンバーが開発しているということもあり、メンテナンスもNuxt本体のアップグレードに合わせて頻繁に行われているので安心です。

導入方法もとても簡単です。
nuxt-vitest をインストール後、 nuxt.config.tsvitest.config.ts にそれぞれ読み込ませたら完了です!

※ バージョンによって導入方法が変わる可能性があるため、詳細は公式の README をご参照ください。

github.com

// nuxt.config.ts の設定例
export default defineNuxtConfig({
  // ...
  modules: [
    'nuxt-vitest'
  ]
})

// vitest.config.ts の設定例
import { defineVitestConfig } from 'nuxt-vitest/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
  },
});

実はこの時点でこのライブラリの恩恵をかなり受けることができており、
vitest.config.ts 内の記述をかなりスッキリさせることができています。

この時点で nuxt-vitest なしで実行しようとした場合、
useNuxtApp や useRoute 等の、 #importsから使用するメソッド類 の import 周りだったり・・・
composables と component のAuto Import 周りだったり・・・
等の問題が出てきてコケてしまいます。

この問題を手動で直すのはかなり難易度が高いため、自動で補完してくれるのはかなり助かりました。

また、 nuxt-vitest には、環境構築用の module だけでなく、
mockNuxtImport 等のユニットテスト用のヘルパーも用意されています。

これらの詳しい使い方については後述させていただきます。

vue-test-utils のマイグレーション作業を実施

ひとまずこの時点で vitest コマンドが正常に実行できる状態にはなるはずなので、
ここから先はspecファイル内の記述の修正を行なっていきます。

公式で Migration Guide が用意されているため、これに沿って実施を行なっていきます。

test-utils.vuejs.org

変更内容としてはそこまで難しくはないのですが、変更量はどうしても多くなってしまう感じでした。

大きいところだと stubsplugins 等のオプションを global の中に入れる必要が出てきたこと等でしょうか。
これが地味に結構大変でした・・。

// before
const wrapper = mount(Component, {
  stubs: {
    ...
  },
})

// after
const wrapper = mount(Component, {
  global: {
    stubs: {
      ...
    },
  },
})

落ちているテストごとの個別修正

この時点である程度テストは通るようになったと思います。
ここから先はテストごとに個別修正をしていきます。

ここから先はプロダクトによって臨機応変に対応を行う必要がありそうですが、
個人的に大変だった部分をいくつかピックアップして記載させていただきます。

Composition API 内で setData メソッドを使用している箇所の修正

vue-test-utils で用意されている、 data の値を変更するメソッドとして setData というものがあります。

test-utils.vuejs.org

このメソッドは v1 の時はComposition API を使用しているコンポーネントでも動いていてくれたのですが、 v2 からはうまく動作がしなくなってしまいました。

そのため、以下のような形で代用することで対応しました。

// before
const wrapper = mount(Component);
await wrapper.setData({ count: 1 });  
expect(wrapper.html()).toContain('Count: 1')

// after
const wrapper = mount(Component);
wrapper.vm.count = 1;
await flushPromises();
expect(wrapper.html()).toContain('Count: 1')

useRoute などの #imports から使用するメソッドのモック化

色々やり方はあると思いますが、
私たちのプロダクトでは、useRoute のモック化を Nuxt 2 環境では以下のような形で行なっていました。

// hoge.vue
<script>
const useRoute();
...
</script>

// hoge.spec.ts
const wrapper = mount(Component, {
  mocks: {
    $route: {
        path: '/hoge',
    },
  },
}) 
...

Nuxt 3 ではこの方法ではモック化ができなくなるため、修正を行う必要があります。

ここで nuxt-vitest に用意されているヘルパーメソッドが活躍します。
mockNuxtImport を使用することによって、以下のような記述で mock 化を行うことができます。

import { mockNuxtImport } from 'nuxt-vitest/utils'

mockNuxtImport('useRoute', () => {
  return () => {
    return {
      path: '/hoge',
    },
  }
})

ここも nuxt-vitest を使わなかったら更にひと工夫がいるであろう箇所のため、かなりの助かりポイントでした。

useRoute 以外のメソッドに関しても、同様の方法で対応を行うことができます。

Vitest の設定を調整し速度改善

なんとかテストが全て通るようになりましたが、
ここでせっかく Jest から Vitest に移行したのにあまり速度が変わっていないことに気づきます。

詳しく調べてみると --single-thread と言うオプションがあることがわかり、
このオプションを有効にしたところ、
冒頭に記載した通り速度が 10分 -> 2分30秒 ( 約 4倍 ! ) にまで跳ね上がりました!

vitest.dev

# コマンドの実行例
yarn vitest --single-thread --coverage

このオプションは名前の通り、シングルスレッドでテストを実行させるオプションで、
ざっくりとまとめると、

  • 無効にした場合 -> テストごとに別々の環境を作成した上で実行される
  • 有効にした場合 -> 同一の環境でテストが実行されるため、初期化を繰り返すコストを回避できる

という挙動を実現してくれるようです。

今回対応を行なったプロダクトは弊社内では規模が大きめのもので、 テストファイル総数 178件、テスト総数 854件 と実行される数も多いです。
そんな中、しかもcoverage付きでこの速度というのはなかなか感動しました・・・ ✨

ただし、公式の Docs に記載されている通り、実行する環境によっては、このオプションを使用した場合うまくいかない場合もあるようなので、そこに関しては注意が必要そうです。

WARNING

Even though this option will force tests to run one after another, this option is different from Jest's --runInBand. Vitest uses workers not only for running tests in parallel, but also to provide isolation. By disabling this option, your tests will run sequentially, but in the same global context, so you must provide isolation yourself.

This might cause all sorts of issues, if you are relying on global state (frontend frameworks usually do) or your code relies on environment to be defined separately for each test. But can be a speed boost for your tests (up to 3 times faster), that don't necessarily rely on global state or can easily bypass that.

最後に

Nuxt 3 本体のマイグレーションについては事例が徐々に出てきてはいるものの、テスト周りはまだ情報が少なくなかなか大変でした。
この記事がみなさまの Nuxt 3 アップグレードの助けになれば幸いです!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp