くりーむわーかー

プログラムとか。作ってて ・試しててハマった事など。誰かのお役に立てば幸いかと。 その他、いろいろエトセトラ。。。

単体テスト

NuxtのテストをCypressでCoverage付でやる

Nuxtのテストがつらい。vue-test-util使って書くのがツライ。

簡単なコンポーネントなら良いと思うのですが、ガチでテスト書こうとすると

mountとか色々上手く動かなかったり、都度アレコレ調べて、「苦肉の策」みたいなコードを

書かないといけないのがつらすぎる。

皆アレでやってるんですかね?書けなくはないんだけど、つらすぎじゃないですか?


という事で、CypressでE2E的に単体テストをやりたくなった話。

Cypressで単体テストをやろうとすると問題になるのが、カバレッジ。

カバレッジが取れないので無理だよなーとか思ってたら、ちゃんと出来るっぽいので組み込んでみた。

最終系は下記に上げてます。

https://github.com/n79s/nuxt-cypress-coverage-sample

参考


まず必要なパッケージのインストール。

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」

sample-cov


どうやってカバレッジ取ってるのかと思ったら、

カバレッジ用のオブジェクト(パスが通ったら該当行のカウントアップするやつ)を

「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側の警告が裏で上がってるので他に良いやり方無いかなー。

Python Djangoで単体テストの結果をマージ

Djangoで単体テストやってる時の話。規模が大きくなると実行対象分割して実行させたくなる。分割の仕方は色々あると思うので割愛。

で、分割実行した場合って結果のcovrageとかも分割されるので、結果をどうにかマージしたい。

jenkinsなんかに表示させてる場合、xunit形式の結果と.coverageが必要になる。

これをどうマージするのかという話。


coverage

Coverage.pyというのがあり、それを使う。というかDjangoの中でも使ってる模様。

普通の単体テストは「python manage.py test」みたいに実行すると思うのですが、

色々やりにくいので、Coverage.pyを通して実行するように変える。↓の感じ。

coverage run --source='.' manage.py test app/tests

で、これだとcoverageのデータファイルが全部「.coverage」になっちゃうので、ファイル名を変えるために下記にする。

COVERAGE_FILE=.coverage_datafile coverage run --source='.' manage.py test app/tests

環境変数の「COVERAGE_FILE」にファイル名指定しておいてから実行するらしい。

そうすると分割したテスト毎にファイル名固定出来るのであとはそれを最後に下記でマージする。

coverage combine .coverage_*

「.coverage_」でファイルを全部指定みたいな。上を実行すると「.coverage」ってデータファイルにまとまるので、あとは、「coverage html」とか「coverage xml」何かで必要なレポート形式に変換する。


xunit

xunitも同様にマージが必要。とりあえず、テストの実行時に下記のオプション指定して結果のファイル名を実行毎に指定してあげる。

manage.py test app/tests/target --with-xunit --xunit-file nose_result_xxx.xml

そしたら、こっちも実行毎にファイルが出来るので、これをマージする。

で、pythonでこれをマージするツール無いかなーって探してみたのですが、下記しかない。

https://pypi.org/project/xunitmerge/

ただ、このツール開発止まってるぽくて更新6年くらい止まってるんですよね。。。

ついでに、python3系で出来ない書き方してるようでそのままだと動かない。

下のプルリクの修正が必要みたい。

https://github.com/miki725/xunitmerge/pull/9

本体に取り込まれる事は無さそうな気がするので、自分でリポジトリ作ってそっち直して、そっちからPIPでインストールする。

GitHubとかGitLabとかリモートリポジトリからpipインストールする場合は↓の感じ。

※別に直で実行してもOK。

pip install  -b release git+https://github.com/***/***/xunitmerge

実行できるようにしたら、下記でマージする。

xunitmerge nosetests_*.xml nosetests.xml

「nosetests_」で始まるファイルを「nosetests.xml」にマージするみたいな。

あとはこれらをjenkinsなりに食わせればOKという寸法です。


最終的にDjangoのtest実行する時のコマンドはもろもろオプションとかつけた感じにすると下記の感じ。

COVERAGE_FILE=.coverage_datafile coverage run --source='.' manage.py test app/tests --settings=dapp.settings_hoge.py --keepdb --with-xunit --xunit-file nosetests_xxx.xml

Vue CLI3+Jest+単体テスト+StoryBook+VisualTest

この前の続き。

まず、StoryBookを使えるようにする。Vueで使う場合はバージョンによって色々やり方があるっぽくて、情報が錯綜しがちで凄いハマル。今現在(2018/11/24)ではこの感じでやればいけそう。

あと、StoryBookでコンポーネント単位の確認用ページを作り、それを使ってビジュアルリグレッションテストをやれるようにする。これは公式のココに書いてあるやり方で基本はやるんだけど、Vueだと動かなかったのでちょっと変える。ついでに、Jestでコンポーネントの単体テスト出来るようにする。

今回のサンプルの最終系は下記にあげてます。作り方の全容も下記に記載してます。あー、あとCentOS7です。

https://github.com/n79s/vue-cli-storybook-sample

動かすためのポイントだけ記載。

まずVue CLI3でプロジェクト作るインストール。

vue create vue-cli-storybook-sample

#手動セッティングで作る↓
Vue CLI v3.1.3
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Linter, Unit
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N)


#上記の構成で作った場合
#Unitテストは↓のディレクトリで作る
tests/unit/****.spec.js

#Unitテスト実行は↓
npm run test:unit

で、StoryBookは↓の感じでインストール。WebPackの設定がどうのこうのって昔はあったんだけど、今現在ではさくっと動く。

#https://storybook.js.org/basics/guide-vue/
npm install --save-dev @storybook/vue
npm install --save-dev babel-core babel-loader babel-preset-vue

#package.jsonに下記を追記
{
    .....
    "scripts": {
        "storybook": "start-storybook -p 9001 -c .storybook"
    }
    .....
}

#.storybookディレクトリを作って設定ファイルを作る
mkdir .storybook

#.storybook/config.js
import { configure } from '@storybook/vue';
import Vue from 'vue';

function loadStories() {
  require('../stories');
  const req = require.context('../stories', true, /\.stories\.js$/);
  req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);

#storybookの実行
npm run storybook

そしたら、ビジュアルリグレッションテストできるようにする。

#必要なパッケージのインストール
npm install --save-dev jest puppeteer jest-puppeteer jest-image-snapshot start-server-and-test

#jest.config.js
module.exports = {
    moduleFileExtensions: ['js','jsx','json','vue'],
    preset: 'jest-puppeteer',
    testRegex: './*\\.test\\.js$',
    setupTestFrameworkScriptFile: './tests/setupVisualTests.js',
    transform: {
        '^.+\\.vue$': 'vue-jest',
        '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
        '^.+\\.js$': 'babel-jest',
    },
    transformIgnorePatterns: ['/node_modules/'],
};

#package.jsonに下記を追記
    .....
    "scripts": {
    "jest:visual": "jest --clearCache && vue-cli-service test:unit -c jest.config-visual.js",
    "test:visual": "start-server-and-test storybook http-get://localhost:9001 jest:visual",
    "jest:visual-update": "jest --clearCache && vue-cli-service test:unit -c jest.config-visual.js --updateSnapshot",
    "test:visual-update": "start-server-and-test storybook http-get://localhost:9001 jest:visual-update"
    }
    .....

で、はまったポイント。

まず、VueCLI3はjestを直で実行するのはサポートしてないから「vue-cli-service test:unit」を使えって、GitHubのIssueに書いてあった(どのIssueだったか見つけられなくなった・・・)。色んなサイトで見てるとだいたい「jest -c ****」みたいなコマンドでやる形になってるので、ここを「vue-cli-service test:unit -c ******」に変えないとだめ。

あと、StoryBookの公式に書いてあるjest.config.jsの記載だと動かないのと、VueCLI3で作られてるjest.config.jsでもダメだった。↓の内容にする

#jest.config.js
module.exports = {
    moduleFileExtensions: ['js','jsx','json','vue'],
    preset: 'jest-puppeteer',
    testRegex: './*\\.test\\.js$',
    setupTestFrameworkScriptFile: './tests/setupVisualTests.js',
    transform: {
        '^.+\\.vue$': 'vue-jest',
        '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
        '^.+\\.js$': 'babel-jest',
    },
    transformIgnorePatterns: ['/node_modules/'],
};

一番、はまったのが、「transform」のとこ。jsをbabel-jestでやるように記載しないとダメっぽい。VueCLI3のIssueに上がってた。ただ、まだ解決してないっぽい。何個か解決策がこのissueに書いてあるんだけど、解決するかは人(環境)によるっぽいですね。ちなみに、これをやってないとテスト実行したときに「SyntaxError: Unexpected token import」がでる。jsファイルをbabelで変換出来てないって事なんかな?あと、transformIgnorePatternsでnode_modulesを除外する設定入れておかないとダメになることもある様子。

issueに書いてある解決策はだいたい↓の感じ。

#transformは↓のどれか。js使えるようにするとの、パスがうまく出来てないかのどっちかかな
'^.+\\.js$': 'babel-jest',
'^.+\\.(js|jsx)?$': 'babel-jest'
'^.+\\.(js|jsx)?$': '<rootDir>/node_modules/babel-jest'

#transformIgnorePatternsは↓のどっちか
transformIgnorePatterns: ['/node_modules/']
transformIgnorePatterns: ['<rootDir>/node_modules/']

#あとはnode_modules一回消してnpm installし直すとか
rm -rf node_modules && npm cache clean --force && npm install

#キャッシュをクリアするとか
"test:unit": "jest --clearCache && vue-cli-service test:unit

自分は通常の単体テストとは分けてjest.config.js作って指定している。あと、jest.config.jsに「preset: 'jest-puppeteer',」入れてないと動かない。あーあと、スナップショットを更新するコマンドは「"jest:visual-update": "jest --clearCache && vue-cli-service test:unit -c jest.config-visual.js --updateSnapshot",」でやる。単体テスト側のスナップショットの更新は↓のコマンド

#単体テスト側のスナップショットの更新は↓(途中の--は間違ってるわけじゃない)
npm run test:unit -- --updateSnapshot

「--」重ねるのがはまった。

次はStoryBookのアドオンを調整するんだけど、StoryBookのReadmeではまった。「webpack.config.js」で最終的に↓の感じにする。

const path = require('path');

module.exports = (storybookBaseConfig, configType, defaultConfig) => {
  defaultConfig.module.rules.push({
    test: [/\.stories\.js$/, /index\.js$/],
    loaders: [require.resolve('@storybook/addon-storysource/loader')],
    include: [path.resolve(__dirname, '../stories')],
    enforce: 'pre',
  });
  defaultConfig.module.rules.push({
    resourceQuery: /blockType=docs/,
    use: [
      'storybook-readme/env/vue/docs-loader',
      'html-loader',
      'markdown-loader',
    ],
  });
  return defaultConfig;
};

で、ビジュアルリグレッションできるようにしたんだけど、自分はコンポーネントはシステム本体とは別プロジェクトにしてやってる。Vuexとか絡むとロジックをかなり書かないといけないので。見た目の変更箇所だけ確認できるようにしたい。SotryBookでその辺は定義して、StoryBookのページを丸ごとGetしてやるだけにする感じ。

普通のe2eのテストでは無く、あくまでも単体テストの一環としてやる感じ。e2eのテストはSeleniumでやる。Chrome拡張で操作記録とってそのまま動かせるので。ただし、操作記録のスクリプトは長い期間保守することを考えるとさすがにイケてないので、調整は必要(要素の指定の仕方とか)。

参考サイト

storybook系
vue-test-utils系
Jest系
GitHubのサンプル
GitHubのissue
問合せ