localStorageを使ったVueプロジェクトのユニットテストがnot definedでコケる件

こんにちは。サービスグループの武田です。

ユニットテスト、書いてますか?ある程度大きくなってしまったプロダクトにゼロからテストを書くのはたいへんですよね。というわけで、小さいうちからテストは書くべきです。今回スモールスタートでlocalStorageを使ったプログラムを書いて、それのユニットテストを書いたところコケてしまいました。調べてみるとVueに限らず、フロントのユニットテストでは あるある なようですので、忘れないためにもエントリにしておきます。

環境

今回の検証環境は次のような環境になっています。

$ node -v
v10.15.3

$ vue -V
3.8.2

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.5
BuildVersion:	18F132

そのほか、主なモジュールのバージョンは以下となっています。また、vueコマンドがインストールされていない場合は、npm install -g @vue/cliでインストールします。

  • vue@2.6.10
  • mocha@5.2.0
  • chai@4.2.0

プロジェクトを作成

まずはVue CLIを利用してプロジェクトを作成します。

$ vue create example-vue-using-localstorage-unittest

presetはManualにして、すべてデフォルトを選択します。ここでテストスイートとしてmochaとchaiが選択されます。

最後の項目を選択するとインストールが始まります。インストールが終われば準備は完了です。

$ cd example-vue-using-localstorage-unittest

カウントアップの実装

今回は、アクセスするたびに回数をカウントアップしていくだけの機能を実装してみます。アクセス数の保存先としてlocalStorageを使用します。

またソースコード全体はGitHubに上げてあります。

テストファーストであれば先にテストを書くわけですが、細かいことは置いておいてまずは動くものを実装しましょう。省エネで進めるため、すでに作成されているAbout.vueを修正して実装します。

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <p>{{ count }}</p>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class About extends Vue {
  get count() {
    return Number(localStorage.getItem('count'));
  }

  private created() {
    const c = localStorage.getItem('count');
    const count = c === null ? 0 : Number(c);
    localStorage.setItem('count', String(count + 1));
  }
}
</script>

特に特筆すべき箇所はありませんが、localStorageのアクセスはgetItem/setItemの利用が推奨されていることを最近知りました。これでアプリケーションを実行してみれば動作が確認できます。Aboutのページにアクセスするたび、数字がカウントアップされていきます。

$ npm run serve -- --open

テストケースの作成と実行

先ほど作成した機能のテストケースを作成します。初回アクセスと複数回アクセスした場合がテストできていればよいのでこんな感じでしょうか?

/* tslint:disable:no-unused-expression */

import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';
import About from '@/views/About.vue';

describe('About.vue', () => {
  it('render 1 with first access', () => {
    const wrapper = shallowMount(About);
    const p = wrapper.find('p');

    expect(p.is('p')).to.be.true;
    expect(p.text()).to.equal('1');
  });

  it('render same count with some access', () => {
    let wrapper = shallowMount(About);
    wrapper = shallowMount(About);
    wrapper = shallowMount(About);
    const p = wrapper.find('p');

    expect(p.is('p')).to.be.true;
    expect(p.text()).to.equal('3');
  });
});

それではユニットテストを走らせてみます。次のコマンドで実行できます。

$ npm run test:unit

結果は次のようになりました。……どうやら、localStorageが未定義で失敗しています。

 WEBPACK  Compiled successfully in 4485ms

 MOCHA  Testing...



  About.vue
[Vue warn]: Error in created hook: "ReferenceError: localStorage is not defined"

found in

---> <About> at src/views/About.vue

localStorageを自前定義して回避

ユニットテストの環境にlocalStorageがないわけですが、自分で定義して回避するのが一番手軽な方法のようです。次のようなブートストラップコードを用意し、テストランナーが読み込むように設定をします。

const localStorageMock = (() => {
  let store = {};

  return {
    getItem(key) {
      return store[key] || null;
    },
    setItem(key, value) {
      store[key] = value.toString();
    },
    clear() {
      store = {};
    },
  };
})();

// global define
localStorage = localStorageMock;

なお、最後のグローバルオブジェクトとして設定している箇所ですが、definePropertyを使った方法はうまく動きませんでした。

Object.defineProperty(window, 'localStorage', {
  value: localStorageMock
});

そして、package.jsonを修正して、自動的にロードされるようにします。

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:e2e": "vue-cli-service test:e2e",
    "test:unit": "vue-cli-service test:unit -r tests/unit/bootstrap.js"
  },

それでは再度テストを走らせます。

 WEBPACK  Compiled successfully in 4232ms

 MOCHA  Testing...



  About.vue
    ✓ render 1 with first access
    1) render same count with some access

  HelloWorld.vue
    ✓ renders props.msg when passed


  2 passing (193ms)
  1 failing

  1) About.vue
       render same count with some access:

      AssertionError: expected '4' to equal '3'
      + expected - actual

      -4
      +3

      at Context.it (dist/webpack:/tests/unit/About.spec.ts:23:1)

localStorage is not defined のエラーは解消されてますね!ただ肝心のテストに失敗しています。これはlocalStorageの状態が残っていることが原因ですね。毎回localStorageをクリアするようにテストを修正してみます。

describe('About.vue', () => {
  beforeEach(() => {
    localStorage.clear();
  });

  it('render 1 with first access', () => {

修正できたらテストを再実行します。

 WEBPACK  Compiled successfully in 4279ms

 MOCHA  Testing...



  About.vue
    ✓ render 1 with first access
    ✓ render same count with some access

  HelloWorld.vue
    ✓ renders props.msg when passed


  3 passing (92ms)

 MOCHA  Tests completed successfully
 

うまく動きました!

まとめ

これからユニットテスト書くぞ!という方の助けになれば幸いです。