Nuxtのテストがつらい。vue-test-util使って書くのがツライ。
簡単なコンポーネントなら良いと思うのですが、ガチでテスト書こうとすると
mountとか色々上手く動かなかったり、都度アレコレ調べて、「苦肉の策」みたいなコードを
書かないといけないのがつらすぎる。
皆アレでやってるんですかね?書けなくはないんだけど、つらすぎじゃないですか?
という事で、CypressでE2E的に単体テストをやりたくなった話。
Cypressで単体テストをやろうとすると問題になるのが、カバレッジ。
カバレッジが取れないので無理だよなーとか思ってたら、ちゃんと出来るっぽいので組み込んでみた。
最終系は下記に上げてます。
https://github.com/n79s/nuxt-cypress-coverage-sample
参考
- https://docs.cypress.io/guides/tooling/code-coverage.html#Introduction
- https://www.cypress.io/blog/2017/11/28/testing-vue-web-application-with-vuex-data-store-and-rest-backend/
まず必要なパッケージのインストール。
npm install --save-dev @cypress/code-coverage nyc istanbul-lib-coverage babel-plugin-istanbul cypress
そしたら一回cypressを動かしてテンプレのファイルとか作ってもらう。
npx cypress open
あとは設定。
- ./nuxt.config.js
build: { babel: { plugins: [['babel-plugin-istanbul']], }, },
- ./.babelrc
{ "env": { "test": { "plugins": ["istanbul"], "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ] ] } } }
- ./nyc.config.js
module.exports = { all: true, extension: ['.js', '.vue'], exclude: ['**/*.{spec,test}.{js,ts}'], include: [ 'pages/**/*.{vue,ts}', 'layouts/**/*.{vue,ts}', 'components/**/*.{vue,ts}', 'module/**/*.js', 'mixin/**/*.js', 'store/**/*.js', ], }
- ./cypress/support/index.js
import './commands' import '@cypress/code-coverage/support' ./cypress/plugins/index.js module.exports = (on, config) => { require('@cypress/code-coverage/task')(on, config) on('file:preprocessor', require('@cypress/code-coverage/use-babelrc')) return config }
あとおまけで、vscodeでcypressのインテリジェンス効くようにする。
- ./jsconfig.json
{ "include": ["./node_modules/cypress", "cypress/**/*.js"] }
あとは適当にコンポーネントとか作って実行すると下記の感じでカバレッジが取れるようになる。
カバレッジはデフォだと「coverage/lcov-report/index.html」
どうやってカバレッジ取ってるのかと思ったら、
カバレッジ用のオブジェクト(パスが通ったら該当行のカウントアップするやつ)を
「window.__coverage__」に丸ごと登録してブラウザ側に渡して、ブラウザで実行される時に、
実行してる行のカウントアップをしてるっぽい。
なるほどなー。
で、これだとブラウザのUIを通してしか実行が出来ない。
例えば、Vuexの値を直で見たい場合とかComponentのmethods何かを直で動かしたい場合にどうするかというところ。
まずVuex。
Nuxtは自身をwindow.$nuxtに登録しているのでそれを下記の感じで取得すれば直で呼べる。
const getStore = () => cy.window().its('$nuxt.$store') getStore().its('state.vcounter').should('equal', 6)//値を直で見る getStore().then((store) => { store.dispatch('setCounter', 10)//アクション呼ぶ getStore().its('state.vcounter').should('equal', 10) cy.wrap(store.getters.vcounter).should('equal', 10) })
Componentは一工夫。というかかなり無茶が必要。
ブラウザ側からやる場合、どうしてもコンポーネントの参照をどこかに持たないと無理。
なので、コンポーネントがmountされる時に、自身をwindowに登録するようにpluginでグローバルミックスインを置いておく。
import Vue from 'vue' if (window.Cypress) { window.$allComponents = [] Vue.mixin({ mounted() { if (this.$vnode) { if (String(this.$vnode.tag).includes('pages')) window.$allComponents.push(this) } }, }) }
通常は入れたくないので、Cypressで動いてる時だけやるようにする。
全コンポーネントでやってもいいと思うのですが、自分はページコンポーネントだけでやってる。
ページコンポーネントの$childrenにそのほかのコンポーネントは入ってるのでそれでいいかなと。
そしたらこんな感じで使う。
const getPageComponent = (pagepath) => cy .window() .its('$allComponents') .then((compos) => { for (const x of compos) { if (String(x.$vnode.tag).includes(pagepath)) { return x } } }) describe('Counter Test Component Direct', () => { it('ClickCounter', () => { cy.visit('http://localhost:3000') cy.get('.button--grey').click() cy.url().should('include', '/sample02') getPageComponent('pages/sample02').then((target) => { cy.wrap(target.$children).its(0).invoke('handleClick')//methods の関数を直呼び cy.wrap(target.$children).its(0).its('vcounter').should('equal', 2)//data()を直参照 cy.wrap(target.$children).its(0).invoke('handleClick') cy.wrap(target.$children).its(0).invoke('handleClick') cy.wrap(target.$children).its(0).its('vcounter').should('equal', 6) }) }) })
ただ、これだとVue側の警告が裏で上がってるので他に良いやり方無いかなー。