なんかいろいろと書いてくブログ

関東のどこかで働く、一般人

【Nuxt】Nuxt × TS環境にJest × TSを導入するためのあれこれ

初めに

私が仕事で開発しているプロダクトではフロントが Nuxt(× TypeScript)で,
サーバー側が C#の JamStack で開発をしています

基本的にはロジックはサーバー側に集約しておりフロントはいわゆる JSON 色付け係状態で、
サーバー側がテストをしっかり書いているのに対して
フロントは全くテストを書いていませんでした

書いていない理由は主に、以下の二つです

  • サーバー側に対して、テストを書く重要度が低かった (TypeScript で型セーフにやれれば下手なテストより安全だろうとも考えていました)
  • Nuxt と Jest、TypeScript の相性が良くなく、書けるまで時間がかかっていた

とはいえ、フロントにテストが全くないのは健全ではないので時間を見つけては
少しづづ導入を進めていいて、なんとか導入するとこまで行けたので その際に苦慮したあれこれを書いていきます

前提

環境について

テストを書く上でか関わってきそうなパッケージのバージョンは以下の通り

  • Nuxt: 2.x
  • Vuetify: 2.x
  • Jest: 27.0
  • TypeScript: 4.3.5

テストフレームワーク

Jest を@types/jest と合わせて使用しています Jest も Type Script で書いていくため、 テスト用のファイルの拡張子は spec.ts になります

その他

API 通信では axios を使用しており、 axios 部分のコードは C#の NSwag から自動生成したものを使っています

プロダクトでは NSwag から自動生成した Interface と axios による API 通信部分が書かれたファイル(ここではopenApi.tsと呼称します)と、 openApi.ts 利用するためのデータの処理を集約したファイル(apiHelper.ts)が存在しています

導入する際、課題になった事項

導入するために、大きな課題になったの事項を挙げます

  • ts が vue ファイルを認識してくれない
  • vuetify への対応
  • this でアクセスするプロパティの mock 化
  • vue ファイルで import している関数の mock 化
  • @types/jest に Nuxt のカスタム Method がない

なぜ、spec.tsなのか(spec.jsではだめなのか)

導入するにあたっての課題の多くはテストを TypeScript で書いていることに起因していました おそらく、JavaScript で書いていればもっと簡単に書いていけたと思います

なのに、なぜ TypeScript で書いていたかと言うと、
単純にプロダクトのメインが TypeScript だからというわけではありません

一番の大きな理由は、NSwag を利用している点にあります 前述したように、NSwag が自動で生成してくれるコードの中には Interface もがあります

Interface には API 通信するために必要なパラメータと返されるパラメータについての定義があります

なので、テストとして axios に、渡す引数のパラメータチェック(型や、引数の数のチェック)を書いておけば サーバー側で使用する model の追加をしても、 フロントのテストで対応できているかのチェックができるというメリットがあります。 そのために TypeScript を採用しています

参考

NSwag についての概要

前置きが長くなりましたが、課題に対してどのように対応したかを、 書いていきます

ts が vue ファイルを認識してくれない

通常、TypeScript は vue を認識してくれず、コンパイルエラーが発生します。 なので、型定義ファイルを用意してあげる必要があります (型定義のない module を使う時と同様です)

vue-shims.d.tsファイルを作成して Vue の型定義を書いていきます (型定義ファイルの名前はなんでも良いです)

declare module "*vue" {
  import Vue from "vue";
  export default Vue;
}

また、vue-shims.d.ts が@types 内ない場合は読み込ませるために tsconfig.jsonにファイルの場所を記載

以下は tsconfig と同じディレクトリにファイルがある場合

files['vue-shims.d,ts']

vuetify への対応

vuetify に限らず、CSS フレームワークで用意されている Component を利用していると、 テスト時ではそのうような Component は知りませんと怒られます

このことに対する対処法はVuetify の公式からも説明があって、

基本的には Vuetify を読み込んで使うことを教えてあげれば良いです

ので、以下を jest.setup.ts に書きます

import Vue from "vue";
import Vuetify from "vuetify";

Vue.use(Vuetify);

また、setup.ts は通常読み込まれないのでjest.config.tsにコードを追加します

以下は test 直下に jest.setup.ts を置いた場合

// <rootDir>はtsconfigで定義
setupFilesAfterEnv: ["<rootDir>/test/jest.setup.js"];

これで、テスト実行時に Vuetify を使用することを宣言できるようになります

参考: setupFilesAfterEnv

this でアクセスするプロパティの mock 化

$axiosや$nuxtのようなVueInstance(多くはthisでアクセスすると思います)に登録しているプロパティにアクセスする関数のテストをする場合は、
vue-test-utilsでmountしただけではテスト時にそんなプロパティ知らんと怒られてしまします

なので、それらプロパティをmockにして、プロパティに登録して上げる必要があります mock化の方法は簡単で、mock化したいプロパティを定義して    マウンティングオプションで指定してあげれば良い
例えば、$axiosをmock化したい場合は下記のように設定します

import { mount } from "@vue/test-utils";
import Vue from "vue/types/umd";
import Index from "~/components/Index.vue";

 describe("test", () => {
    test("hogehoge'", () => {

      const $axios = jest.fn()
      
      const wrapper =  mount(Index, {
        mocks: { $axios },
      });
 });

const $axios = jest.fn()

の部分はjest.fn()でなくても良いのですが、(const $axios = {}とかでもよかったはず)
実際にaxiosが参照したかどうかを見るときがあると思うので、 ここではjest.fn()にしています

また、mockはネストしたプロパティについてもmock化することが可能で、 例えば、axios.CancelToken.source()をmock化したければ以下のように$axiosの中身を定義すれば良いです

const cancelTokenSource = { cancel: jest.fn(), token: { reason: { message: "message" } } };

const $axios = {
  CancelToken: {
    source: () => {
      return cancelTokenSource;
    },
  },

この場合、テスト実行時、axios.CancelToken.source()は最初に定義したcancelTokenSourceを返すmockになります

参考

mocks

vue ファイルで import している関数の mock 化

helper関数のように、関数を共通処理としている場合、これらもmockにしたいことがあります
その場合、jest.mockを使用して、importしているファイルごとmock化してあげます

// index.vue
<script lang="ts">
import Vue from "vue";
import { getHelloWorld } from "~/helpers/helloWorld";

export default Vue.extend({
  methods: {
    helloWorld() {
      return getHelloWorld()   
    },
  },
});
</script>
// index.spec.ts
jest.mock("~/helpers/helloWorld");

describe("test", () => {
  test("hogehoge'", () => {  
    const wrapper =  mount(Index);
  })
});

ただし、これだけだと以下のようなデメリットがあります - トップレベルでした使用できない - すべての関数返り値がundefinedとなる

そのため、関数を特定の値を返すようにしたり、その関数の返り値をテストごとに変えたい場合はmock化した後に さらに、関数のmockを定義する必要があります

jest.mock("~/helpers/helloWorld");
import { getHelloWorld } from "~/helpers/helloWorld";

describe("test", () => {
  test("hogehoge'", () => {  

    //getHelloWorld()をjest.MockedFunctionに型アサーション
    const mock = getHelloWorld as jest.MockedFunction<typeof getHelloWorld>;
    
    // 返り値を設定
    mock.mockImplementation(async () => {
      return "Hello World";
    });
    const wrapper =  mount(Index);
  });
})

jest.mock("~/helpers/helloWorld");

で全体をmock化した後に、importしている関数をmock化しています

const mock = getHelloWorld as jest.MockedFunction;

その後、mockImplementationで返り値あるmockの実装をしています

参考

mock関数

@types/jest に Nuxt のカスタム Method がない

通常、vueファイルのmethod内の関数を直接実行する場合は mountしたVueInstanceにvmでアクセスしてさらに、関数名で参照します

const wrapper =  mount(Index);

// method内のhoge関数にアクセス
wrapper.vm.hoge()

しかし、TypeScriptで書く場合にはhogeという関数は@typesには当然定義されていないので   コンパイルエラーとなります

一応、vmをanyにアサーションすることによりコンパイルエラーを回避することができますが
anyは存在することがよくないので、 mount()の返り値であるvmを拡張し、関数を定義して対応しました

import { mount, Wrapper } from "@vue/test-utils";

interface VueExtend extends Vue {
  hoge: Function;
}

interface WrapperExtend extends Wrapper<Vue, Element> {
  vm: VueExtend;
}

describe("test", () => {
  test("hogehoge'", () => {  
    const wrapper =  mount(Index);
    wrapper.vm.hoge()

  });
})

参照

Wrapper の型定義(Github)

終わりに

Nuxt × TypeScriptに対して、Jest × TypeScriptでのテストコード実装をして、 Nuxt とTypeScripとの相性の悪さを改めて実感しました

Vue3はTypeScripとの相性の悪さは改善されており、 Vue3なら、もっと書きやすくなるかもしてません (だからと言って、Vue3を使用するかというと考えどこですが)

また、上記に挙げた対処法はベストプラクティスとは思っておらず、
まだまだ、改善の余地がありそうです