6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Nuxt.js + TypeScript +Jest でテストをする時に詰まったエラーと対応方法

Last updated at Posted at 2021-03-11

はじめに

Nuxt.jsとTypeScriptで構築したプロジェクトに、Jestでテストを書く事になりました。
その際に引っかかった部分についてまとめています。
まだしばらくJestでテストを書く予定なので、順次追記していきます。

初期設定

i18nの文字列が取得できない

this.$tはfunctionではないというエラーが表示され失敗するパターン

console.error
  [Vue warn]: Error in render: "TypeError: this.$t is not a function"

この場合は$tにMockを設定することで回避が可能です。
また、i18nはプロジェクト全体で使用していることが多いと思うので、グローバルなMockに設定すると良いです。

jest起動時に実行する処理の設定

ファイル名は何でも良いのですが、ここではjest.setup.tsというファイル名でjest.config.tsと同階層に作成します。
jest.setup.tsjest.config.tssetupFilesというプロパティに設定することで、jest起動時に呼び出すように指定します。

jest.config.ts
import type { Config } from '@jest/types'

const config: Config.InitialOptions = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js',
  },
  moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest',
  },
  setupFiles: ['./jest.setup.ts'], // 追加
}

export default config

jest.setup.tsでは、i18nのMockを作成してconfig.mocksに設定します。
これで全てのjestファイルのmockにi18nが適用されるようになります。
i18nMockの処理は、単純に渡されたkeyに対してjsonから文字列を返しているだけのメソッドです。

jest.setup.ts
import { config } from '@vue/test-utils'
import locales from '@/assets/json/locales/ja.json'

// i18nのモックを追加
const i18nMock = (key: string) => locales[key as keyof typeof locales]

const mocks = {
  $t: i18nMock,
}
config.mocks = mocks

nuxt-linkが解決できない

nuxt-linkも同様にstubに置き換える必要があります。
こちらも先程のjest.setup.tsに設定を追加すると良いです。
nuxt-linkには、@vue/test-utilsからRouterLinkStubというものが提供されているため、それをimportしてきて使用します。

jest.setup.ts
import { config, RouterLinkStub } from '@vue/test-utils'
:
省略
:
// nuxt-linkのstubを設定
config.stubs['nuxt-link'] = RouterLinkStub

基本的な使用方法

dataが呼び出せない

dataを触る場合は、wrapper.vm.$dataを参照します。

expect(wrapper.vm.$data.hoge).toBe('abc')

methodやcomputedが呼び出せない

methodやcomputedはdataと異なりvmから提供されていないため、vmをanyにキャストするか、
変数定義の際に型情報を追加してあげる必要があります。

キャストする方法は単純にメソッドを呼び出す時にanyにキャストします。

anyにキャストする方法
expect((wrapper.vm as any).increment(3)).toBe(3)

型情報を追加する方法は、変数宣言時に & { [key: string]: any }とすることで、
vmにVueクラスのメソッド以外にも存在することを定義します。
こちらの方が、Vueクラスのプロパティが消えないことや、1箇所の定義ですむことからより良いかと思います。

型情報を追加する方法
let wrapper: Wrapper<Component & { [key: string]: any }>
wrapper = shallowMount(Component, { propsData: { ...defaultProps } })
expect(wrapper.vm.increment(3)).toBe(3)

個人的には、factoryメソッドを作成して、その中でwrapperを作成するのがおすすめです。

factoryを作成する方法
// wrapperを生成するfactory
const factory = (propsData?: PropsDataInterface) =>
  shallowMount<Component & { [key: string]: any }>(Component, {
    propsData: { ...defaultProps, ...propsData },
  })

// wrapperにデフォルト値から変更するプロパティのみ渡す
const wrapper = factory({ ...defaultProps, items })

slotに値を入れる

slotに値を入れるには、shallowMountの第2引数にslotsというプロパティを渡すと設定ができます。
slot名は指定しない場合はdefaultとなるため、slots: { default: 'slot contents' },という値を設定すると反映されます。

describe('BasicButton', () => {
  const factory = (propsData?: PropsDataInterface) =>
    shallowMount<Component & { [key: string]: any }>(Component, {
      propsData: { ...defaultProps, ...propsData },
      slots: { default: 'slot contents' },
    })

  test('slotにコンテンツが挿入できること', () => {
    const wrapper = factory()
    expect(wrapper.html()).toBe('<button>slot contents</button>')
  })
})

Emitを検知する

Emitがされたかを検知するには、wrapper.emitted('emit名')を使用すると検知が出来ます。
emittedの戻りは2次元配列で表現されており、emit発火 * emitの内容となります。


await wrapper.vm.click()
await wrapper.vm.click()
wrapper.emitted('success')
/*
[
  [
    "1回目のEmitの引数",
    "1回目のEmitの戻り値",
  ],
  [
    "2回目のEmitの引数",
    "2回目のEmitの戻り値",
  ],
]
*/
expect(wrapper.emitted('success')).toStrictEqual([[true, true], [true, true]])
// emitされなかった場合はundefinedが設定される
expect(wrapper.emitted('failed')).toBeUndefined()

応用/エラー対応

[Vue warn]: Unknown custom element

テスト対象の.vueファイルにコンポーネントが存在する場合に発生しました。
対応としては、shallowMountの際にstubsを設定することで回避ができます。

 console.error
    [Vue warn]: Unknown custom element: <FooComponent> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
const factory = (propsData?: PropsDataInterface) =>
  shallowMount<Component & { [key: string]: any }>(Component, {
    propsData: { ...defaultProps, ...propsData },
    // stubを追加、コンポーネント名の文字列を渡す
    stubs: ['FooComponent'],
  })

this.$router.pushの確認

$router.pushにMockを設定するが、Mockにjest.fn()を使用します。
jest.fn()を使用することで、実行された回数や引数などが取得できるようになります。

// $router.pushのモックを定義
const pushMock = jest.fn()
const $router = { push: pushMock }


const factory = (propsData?: PropsDataInterface) =>
  shallowMount<Component & { [key: string]: any }>(Component, {
    propsData: { ...defaultProps, ...propsData },
    // mockを追加
    mocks: { $router },
  })

test('クリック時の動作', async () => {
  // toというPropにpathを渡すとそのpathに$router.pushするコンポーネント
  const wrapper = factory({ to: '/hoge' })
  // buttonタグを探してクリック
  const button = wrapper.find('button')
  await button.trigger('click')
  // $router.pushが1回呼ばれたことの確認
  expect(pushMock.mock.calls.length).toBe(1)
  // 引数が '/hoge' であることの確認
  expect(pushMock).toHaveBeenCalledWith('/hoge')
})

参考記事

6
3
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
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?