【Nuxt.js】@nuxtjs/composition-apiでuseContextから読み込んだaxiosをテストする

@nuxtjs/composition-apiを使ったプロジェクトで、$axios.postを発火するメソッドをテストをする際にかなり詰まったので記事にします。

検証環境
  • nuxt: 2.15.3
  • @nuxtjs/composition-api: 0.22.3
  • @nuxt/typescript-build: 2.1.0
  • jest: 26.6.3
  • @vue/test-utils: 1.1.3


ざっくり結論

Vue Test Utilsのマウンティングオプションで$nuxtcontextをモックする



$axiosが読み込まれない問題

useContext()から$axiosを読み込み、postgetを叩く実装をしている場合、テストで$axiosを読み込んでくれないことがあります。

<template>
  <div>
    <h1>axios</h1>
    <el-button round @click="axiosMethod">button</el-button>
  </div>
</template>

<script lang="ts">
import { defineComponent, useContext } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const { $axios } = useContext()

    const axiosMethod = async () => {
      await $axios.post('api/hoge')
    }

    return {
      axiosMethod,
    }
  },
})
</script>

<style></style>
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Element from 'element-ui'
import AxiosPage from '@/pages/form/axios.vue'

const localVue = createLocalVue()
localVue.use(Element)

const mockAxios = {
  post: jest.fn(),
}

describe('Axios', () => {
  let wrapper

  beforeEach(() => {
    wrapper = shallowMount(AxiosPage, {
      localVue,
      mocks: {
        $axios: mockAxios,
      },
    })
  })

  describe('単体テスト', () => {
    describe('axiosMethod', () => {
      it('POST api/hogeが叩かれる', async () => {
        await wrapper.vm.axiosMethod()
        expect(mockAxios.post).toHaveBeenCalledTimes(1)
      })
    })
  })
})
  ● Axios › 単体テスト › axiosMethod › POST api/hogeが叩かれる

    TypeError: Cannot read property 'post' of undefined

      14 |
      15 |     const axiosMethod = async () => {
    > 16 |       await $axios.post('api/hoge')
         | ^
      17 |     }
      18 |
      19 |     return {

Option apiでは上記のテストで通ります(多分)。

composition apiにおけるjest

jestはNuxtのcontextを知らないので、contextから読み込む$axios$nuxtとしてmockしてあげる必要があるようです。

// ・・・

const mockAxios = {
  post: jest.fn(),
}

describe('Axios', () => {
  let wrapper

  beforeEach(() => {
    wrapper = shallowMount(AxiosPage, {
      localVue,
      mocks: {
        $nuxt: {  // 追加
          context: {  // 追加
            $axios: mockAxios,
          },
        },
      },
    })
  })
  // ・・・

})
$ yarn test
yarn run v1.22.10

$ jest --config jest.config.js
 PASS  test/axios.spec.js
  Axios
    単体テスト
      axiosMethod
        ✓ POST api/hogeが叩かれる (53 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.24 s
Ran all test suites.

✨  Done in 5.19s.

通りました!

ついでにcontext.rootについて

$axios(などnuxtのcontextに依存するもの)を利用したい場合、以下のようにsetupメソッドの第二引数からcontextを受け取り、rootから読み込むことが推奨されている文献をよく見かけます。

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  setup(_, context) {
    const axiosMethod = async () => {
      await context.root.$axios.post('api/hoge')
    }

    return {
      axiosMethod,
    }
  },
})
</script>

rootからの読み込みはVue3で非推奨の書き方のようです。

非推奨メッセージ(エディタ)

@nuxtjs/composition-apiでは基本的にuseContextから使うので大丈夫だと思いますが、composition api導入当初は戸惑いポイントだったので、気をつけたいと思います。

参考

fix: jest test reports error when encountering `useContext` · Issue #248 · nuxt-community/composition-api · GitHub

【Nuxt.js】Jest実行時「 You need to add `@nuxtjs/composition-api` to your buildModules in order to use it. See https://composition-api.nuxtjs.org/getting-started/setup.」でテストが実行されない

@nuxtjs/composition-apiを使用したプロジェクトでテストを実行する際、表題のエラーでテストが実行されないことがあります。

実装ではnuxt.config.tsbuildModules@nuxtjs/composition-apiを記載していると思いますが、Jestにも@nuxtjs/composition-apiのエントリーポイントを教えてあげる必要があります。

module.exports = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js',
    '@nuxtjs/composition-api': '@nuxtjs/composition-api/lib/entrypoint.js', // これを追加
  },
  moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest',
  },
  collectCoverage: true,
  collectCoverageFrom: [
    '<rootDir>/components/**/*.vue',
    '<rootDir>/pages/**/*.vue',
  ],
}

【Vue.js / Nuxt.js】 .sync修飾子で状態の更新を簡単に行う

今更感が溢れる内容ではありますが、最近初めて .sync修飾子 を使ってみて、コンポーネント/page がすっきりしたので、まとめてみます。

検証環境
  • nuxt: 2.15.3
  • @nuxtjs/composition-api: 0.22.3
  • @nuxt/typescript-build: 2.1.0
  • element-ui: 2.15.1

.sync修飾子とは

jp.vuejs.org

親pageや親コンポーネントで用意した状態を、子からイベント経由で更新したい場合に簡単に実装ができるシンタックスシュガーです。

そもそも子から親の状態は直接更新できない

プロパティ — Vue.js

単方向のデータフロー 全てのプロパティは、子プロパティと親プロパティの間に 単方向のバインディング を形成します: 親のプロパティが更新されると子へと流れ落ちていきますが、逆向きにデータが流れることはありません。これによって、子コンポーネントが誤って親の状態を変更すること(アプリのデータフローを理解しづらくすることがあります)を防ぎます。

vueの基本ですが、単方向のデータフロー制約があるため、親から渡ってきた状態を子が直接更新することはできません。子側無理やり更新すると以下のコンソールエラーで怒られます。

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. 
Instead, use a data or computed property based on the prop's value. Prop being mutated: "formData"

found in...

じゃあ更新するためにどうするかと言うと、emitによるカスタムイベントを発火させて親に通知し、親で更新してもらう必要があるわけですね。

<template>
    <el-input
      :value="props.formData"
      @input="updateFormData"
    ></el-input>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    formData: {
      type: String,
      required: true,
      default: '',
    },
  },

  setup(props, { emit }) {
    const updateFormData = (input: string) => {
      emit('updateFormData', input) // 親に通知
    }

    return {
      props,
      updateFormData,
    }
  },
})
</script>

<style lang="scss" scoped></style>
<template>
    <FormPlaySync
      :form-data="state.formData"
      @updateFormData="updateFormData"
    />
</template>

<script lang="ts">
import { defineComponent, reactive } from '@nuxtjs/composition-api'
import FormPlaySync from '~/components/FormPlaySync.vue'

export default defineComponent({
  components: {
    FormPlaySync,
  },

  setup() {
    const state = reactive({
      formData: '',
    })

    const updateFormData = (input: string) => {
      state.formData = input
    }

    return {
      state,
      updateFormData,
    }
  },
})
</script>

<style lang="scss" scoped></style>

普段Vueで開発されている方からするとお馴染みかもしれません。

ですがこれだと、子から渡ってきた値をそのまま親の状態に反映させるだけのメソッドが親に生やされ、少し冗長にも思えます。親のコード量によっては見通しが悪くなってしまうかもしれません。

そこで使用したいのが .sync修飾子です。

.sync修飾子を使って書き換えてみる

親では子にバインドしたい状態に.syncを加え、子ではイベント名をupdate:props名に変更します。こうすることで、親側で代入処理を明記することなく自動的に状態を更新してくれます。

<template>
  <div class="FormPlaySync">
    <h2 class="title">FormPlaySync</h2>
    <el-input :value="props.formData" @input="updateFormData"></el-input>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    formData: {
      type: String,
      required: true,
      default: '',
    },
  },

  setup(props, { emit }) {
    const updateFormData = (input: string) => {
      emit('update:formData', input) // イベント名を 'update:props名' に変更
    }

    return {
      props,
      updateFormData,
    }
  },
})
</script>

<style lang="scss" scoped></style>
<template>
  <div class="FormPlayGroundPage">
    <h1 class="title">FormPlayGroundPage</h1>
    <FormPlaySync :form-data.sync="state.formData" /> // .syncを追加、イベント監視を削除
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from '@nuxtjs/composition-api'
import FormPlaySync from '~/components/FormPlaySync.vue'

export default defineComponent({
  components: {
    FormPlaySync,
  },

  setup() {
    const state = reactive({
      formData: '',
    })
    // 状態を更新するメソッドを削除
    return {
      state,
    }
  },
})
</script>

<style lang="scss" scoped></style>

親側がスッキリしました!

propsがオブジェクトの場合は以下でできます。

<template>
  <div class="FormPlaySync">
    <h2 class="title">FormPlaySync</h2>
    <p>オブジェクト</p>
    <el-input
      :value="props.piyo"
      @input="updateFormDataObject('piyo', $event)"
    />
    <el-input
      :value="props.fuga"
      @input="updateFormDataObject('fuga', $event)"
    />
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    piyo: {
      type: String,
      required: true,
      default: '',
    },
    fuga: {
      type: String,
      required: true,
      default: '',
    },
  },

  setup(props, { emit }) {
    const updateFormDataObject = (key: string, value: string) => {
      emit(`update:${key}`, value)
    }

    return {
      props,
      updateFormDataObject,
      updateFormDataPrimitive,
    }
  },
})
</script>

<template>
  <div class="FormPlayGroundPage">
    <h1 class="title">FormPlayGroundPage</h1>
    <FormPlaySync v-bind.sync="state.formData" /> // v-bind.syncに変更
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from '@nuxtjs/composition-api'
import FormPlaySync from '~/components/FormPlaySync.vue'

export default defineComponent({
  components: {
    FormPlaySync,
  },

  setup() {
    const state = reactive({
      formData: {
        piyo: '',
        fuga: '',
      },
    })

    return {
      state,
    }
  },
})
</script>

子のpropsでオブジェクトのキーをそれぞれ定義し、inputイベントでkeyとvalueをパラメーターとして送ってあげて、emit(update:${key}, value)で更新ができます。

v-modelでもできる

ちなみに親側で、v-bindではなくv-modelを使用して、子のイベントを直接検知することも可能です。 しかし、Vue2だと子で受け取るprops名はvalueにしなければいけない、emit時のイベント名はinputにしなければいけない、などの制約があるようで、少し使いにくいです。

jp.vuejs.org

ただ、Vue3だと上記制約が解消され、.sync修飾子は削除、v-modelの使用に統一されているようです。

v3.ja.vuejs.org

Composition apiにおける.sync修飾子やv-model

個人的にですが、Composition Functionにモデルとそのロジックをまとめたい場合は .sync修飾子を使わず、子から親にイベント通知後、Composition Function内にあるメソッドを発火してモデルを更新するように実装しています。

Composition apiを使用したモデル層の切り出しにおいては、モデルに関するロジックは1箇所にまとめておくことで、よりComposition apiの良さを引き出せるのではないかと考えているためです。


終わりに

親子間の通信がある場合、かつComposition Functionを使用しない場面において、.sync修飾子はコンポーネントの見通しを良い状態に保ついい手段なのではないかと思いました。子側で算出プロパティのgetterとsetterを利用する方法もありますが、.sync修飾子の方が簡潔な気がします。

今後も機会があれば使っていこうかと思います。

参考

自己紹介

f:id:itoka_pi:20210412225854j:plain

簡単に自己紹介します。

現在の仕事

新人エンジニア1年目です。

コロナ禍の2020年6月から、都内のスタートアップ企業で正社員Webエンジニアとして働いています。

普段は受託のスクラムチームでアジャイル型開発を行なっています。自社のサービスも持っている会社なので、社内副業としてそちらの開発をしていることもあります。

経験案件数は働き始めてから現在2件目で、2021年の3月まではずっと同じ会社さんの受託案件に参画していました。

コロナの影響もあり、現在はフルリモートで働いています。スクラムなので完全に自由とはいかないですが、フレックスタイムなので比較的自由が効く環境だと思います。

働く人も穏やかな方が多く、SNSや新人エンジニアのブログで見るようなきつい当たり方をする先輩エンジニアはいません。人にはかなり恵まれていると思います。

ちなみに人が足りていないので、部署周りの企画やエンジニア採用などもやっています。

PCは自前のMacBookPro2015です。そろそろきつくなってきた。

過去の仕事

新卒から2021年2月までは都内の金融系企業で働いてました。経験としてはマーケティング・webマーケティング、事業企画、商品開発など、幅広く仕事していた気がします。自社会員向けのスマートフォンアプリケーションなんかも企画からローンチまでやってたりしました。

2020年辺り、ちょうど転職を考えていたタイミングで親会社の銀行へ出向が命じられるなど、エキサイティングな年を過ごしていました。ちなみに出向先では経営企画をやってました。

業務柄Webに触れる機会が多かったことや、昔からPCゲームで遊んでいたので、もともとWebには親しみを覚えていたと思います。

個人でスキルを学ばないと生き残れない時代だとか、自分でものつくりをすることが楽しいだとか、スキルアップを感じられることがたのしいだとか、そんな感じの理由を考えてWebエンジニアへのキャリアチェンジを考えました。

技術分野

バックエンド

入社直後からは、運良く実務未経験の頃から勉強していたPHP / Laravelの案件にアサインしてもらえました。業務用Webアプリケーションでしたが、一つのWebアプリケーション内に複数のWebアプリケーションを運営しているサービスだったため、言語としてはCakePHPも触っていました。

2020年11月から社内副業案件にアサインされ、Ruby / Ruby On Railsで開発するようになりました。未経験言語だったので不安はありましたが、PHPやLaravelを触っていたからか、理解するのは割と早かった気がします。

2021年4月からは本業案件の異動があり、メイン言語としてRuby On Railsを触っています。

フロントエンド

フロントエンドは完全未経験で入社しましたが、入社当時からVue / Nuxtで開発しています。

当時はフロントエンドとバックエンド別々での開発、つまりJSでの開発経験が全くなく、AjaxAPIってどう作るの?ナニ??みたいなスキル感でした(API自体の概念は前職で触れていたので知ってましたが…)。

この間までは趣味でReactを書いていたりしましたが、4月からはNuxtでTypeScriptを使うようになったため、比重としてはTypeScriptの勉強に時間が寄っています(入社当時からしたら考えられないスキルアップ感。。)。

インフラ

インフラの知識は今でも乏しいです。実務未経験の時代、ポートフォリオの本番環境としてAWSを立ち上げたぐらい。使っていたのはEC2、RDS、ELB、Route53、IAM、S3、ACM辺りでした。

その他

TDD(BDD)

全社としてTDDを積極推進しており、入社当時からPHPではPHPUnitRubyではRspec、NuxtではJestを使ったTDDで開発してきました。当時はJSすらほぼわからなかったところにテストが加わってきたので、毎日ヒーヒー言いながら開発していました。

ですが、そのおかげで今はそれなりにテスト駆動で書けているのではないかと思います。楽しい。

RSpecではモデルスペックとリクエストスペック、Jestではホワイトボックステスト単体テスト)・ブラックボックステスト結合テスト)を書いています(4月からの案件ではJestは薄め)。

CI

よくCI構築、移行もやってました。LaravelではCircleCIからGithub Actionsに移行、Nuxtは一からGithub Actionsを構築しました。

Docker

めちゃくちゃ詳しい、というわけではないですが、実務でもちょくちょく調整したりして触っていました。環境構築程度であればできます。本番でのDocker運用経験はありません。 Docker自体は好きです、仮想環境という響きが何だか格好いいのと楽しいから。

苦手分野

マークアップCSSは苦手です。いずれWeb制作副業などもやってみたい、ちょくちょく勉強していきます。


簡単ではありますが、自己紹介でした。

誰が見るの、という感じではありますが、Webエンジニアになって11ヶ月目、そろそろ1年が経つので振り返りとしてちょうど良かった。

これからは開発時のメモや、これまでに読んだ本の紹介等が書ければいいなと思っています。

技術ブログを開設しました

技術ブログとはいえ、メモ書き程度かもしれません。

以前も作っていましたが、全く更新せず放置していたので、このブログも同じ道を辿る可能性はあります。

・・・

なぜブログを作ったか

ブログを開設するモチベーションは人によって様々かと思いますが、私の場合は以下の通りです。

  1. 「やっぱりエンジニアは技術記事書いてアウトプットだよね!」にノせられた
  2. 普段の業務中に取っているメモをネタにして記事公開できるのではないか
  3. ブログを自分のポートフォリオとして資産にできる可能性がある

特に理由として大きいのは3かもしれません。

最近は業務委託の方のエンジニアの採用を担当する機会が増え、応募してきた方のプロフィールを拝見しています。応募してきた方の技術レベルを把握するにはしっかりとしたポートフォリオが公開されていることが重要だと感じるようになりました。

ほとんどの業務委託マッチングサービスでは専用のスキルマップを作成でき、採用者が確認することもできます。 ただ、個人的にはコードベースで技術レベルを確認できること、かつgithubというよりかは個別のテーマや取り組みについて記載されているブログ記事の方が、その方のスキル感がわかりやすいと感じるようになりました。

自分もエンジニア人生におけるポートフォリオとして技術をオープンにし、資産にしたいなと考えるようになりました。

どれくらいの頻度で書くか

まずは2〜3ヶ月に1回を目指したい。

頻度低いですが、色々な技術ブログを拝見していても意外とこれぐらいのペースで更新されていたりしたので、まずはこの程度から。

何事もまずはやってみることが大事だと思う、これホント。量や質を最初から気にしない。

何を書くか

基本的には日々の開発業務で得た技術知識や、調べた内容を書いていく予定ですが、キャリアやその他雑記などもあるかもしれません。そのための「開発、いろいろ」です。

おそらく私の次の記事は自己紹介記事な気がします。

ブログのテーマを何にするか迷いましたが、専門的に振り切るか様々分散して書くかは、調べてみると皆さんブログ開設時共通の悩みみたいでした。

終わりに

ブログ初心者なので、どういった風に書けばいいかあまりイメージついていないのが正直なところではありますが、手探りで頑張ってみようと思います。

よろしくお願いします!