Motomichi Works Blog

モトミチワークスブログです。その日学習したことについて書いている日記みたいなものです。

JSでテストコードを書く その0003-01 karma+mocha+chaiの組み合わせでCircleCI上でユニットテストを実行する

参考にさせて頂いたページ

全体的なこと

karmaのこと

はじめに

Railsで生成されるapplication-xxxxxxxx.jsみたいなのをテストしたい目的があり、テスト対象のファイル名がapplication-xxx.jsですが、色々応用が利くかと思います。

リモートとローカルのリポジトリを作成する

package.jsonを作成していくつかのパッケージをインストールする

  • npm initして、今回はデフォルトのままenterしていって、yesで大丈夫。
  • npm install --save-dev phantomjs karma karma-cli karma-mocha karma-phantomjs-launcher karma-mocha-reporter mocha chai

.gitにnode_modulesディレクトリをpushしないようにする

  • .gitignoreファイルが無かったら作成する。
  • .gitignoreファイルに/node_modulesを追記する。

js/application-xxx.jsを作成する

自分が作っているwebアプリケーションのjsファイルを想定しています。

window.addNumbers = function(a, b) {
  return a + b;
};

spec/application.spec.jsを作成する

自分が作っているwebアプリケーションの関数を検証するためのソースコードです。

describe('addNumbers', function() {
  it('2 つの数値が加算できる', function() {
    chai.assert.strictEqual(window.addNumbers(1, 2), 3);
  });
});

karma.conf.jsを作成する

./node_modules/karma/bin/karma init karma.conf.jsを実行して対話形式で作成するか、手で作成する

module.exports = function(config) {
  config.set({
    frameworks: ['mocha'],
    browsers: ['PhantomJS'],
    files: [
      'js/application*.js',
      'node_modules/chai/chai.js',
      'spec/**/*.spec.js',
    ],
    reporters: ['mocha'],
  });
};

以下のことを設定しています。

  • フレームワークはmocha
  • ブラウザはPhantomJS
  • filesでテスト対象ファイルと、テストで使用するファイルを読み込み
  • reportersでコンソールのテスト結果出力フォーマットを指定

package.jsonのscriptsのtestを編集する

"scripts": {
  "test": "./node_modules/karma/bin/karma start --single-run"
},

コマンドでテストを実行してみる

  • ./node_modules/karma/bin/karma start --single-runユニットテストが実行できます。
  • package.jsonの設定によってnpm run testでも同じように実行できるはずですが、windowsだとエラーが出ました。CircleCIはこのコマンドを自動で実行してくれます。

pushしてCircleCIによるビルドを実行する

リポジトリにpushすることでCircleCIによるビルドが始まります。

CircleCIはnpm installを自動でやってくれるので、package.jsonに書いてあるdevDependenciesが予めインストールしたうえで、npm run testコマンドが実行されます。

ユニットテストが全て成功して、CircleCIのステータスが「SUCCESS」になったら自動テストの設定は成功ですね。

pushする前には./node_modules/karma/bin/karma start --single-runを一度実行して、ローカルでユニットテストが成功してからpushするのが良いですね。

まとめ

今回の最終的なディレクトリ構造

practice_circle_ci_karma/
  ├─.gitignore
  ├─karma.conf.js
  ├─package.json
  ├─README.md
  ├─.git/
  │  └─(略)
  ├─js/
  │  └─application-xxx.js
  ├─node_modules/
  │  └─(略)
  └─spec/
     └─application.spec.js

karma.conf.jsの記述内容

module.exports = function(config) {
  config.set({
    frameworks: ['mocha'],
    browsers: ['PhantomJS'],
    files: [
      'js/application*.js',
      'node_modules/chai/chai.js',
      'spec/**/*.spec.js',
    ],
    reporters: ['mocha'],
  });
};

package.jsonの記述内容

{
  "name": "practice_circle_ci_karma",
  "version": "1.0.0",
  "description": "karmaを導入して、CircleCIで実行するサンプルを作成してみます。",
  "main": "index.js",
  "scripts": {
    "test": "./node_modules/karma/bin/karma start --single-run"
  },
  "repository": {
    "type": "git",
    "url": "xxxxxx/practice_circle_ci_karma.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "xxxxxx/practice_circle_ci_karma/issues"
  },
  "homepage": "xxxxxx/practice_circle_ci_karma#readme",
  "devDependencies": {
    "chai": "^4.0.1",
    "karma": "^1.7.0",
    "karma-cli": "^1.0.1",
    "karma-mocha": "^1.3.0",
    "karma-mocha-reporter": "^2.2.3",
    "karma-phantomjs-launcher": "^1.0.4",
    "mocha": "^3.4.2",
    "phantomjs": "^2.1.7"
  }
}

JSでテストコードを書く その0002-01 mochaとchaiでテストを書いてブラウザでテストを実行する

参考にさせて頂いたページ

日本語の参考ページ

mocha公式

chai公式

はじめに

日本語の情報を読んで書いてみましたが、最新の情報を公式でチェックすると良いでしょう。

jsのテストを手軽に導入する方向で、最も簡易的な方法に着手してみました。

js_testing_0001.htmlを作成して、ブラウザで開くとテストが実行できます。

mocha.js mocha.css chai.jsをダウンロードする

  • とりあえずjs_testingフォルダを作りました。
  • js_testingフォルダの中でnpm initします。今回はデフォルトのままenterしていって、yesで良いです。
  • mocha.js mocha.css chai.jsの3つが欲しいのでnpm install --save-dev mocha chaiでダウンロードします。
  • これらはのちほどhtmlファイルの中で読み込みます。

js_testing/js/application.js を作成する

自分が作っているwebアプリケーションのjsを想定しています。
記述内容は例として以下の通りです。

window.addNumbers = function(a, b) {
  return a + b;
};

js_testing/spec/add_numbers.spec.js を作成する

上記したaddNumbers()関数が意図どおり動いているか検証するためのコードを書きます。
例として以下の通りです。
引数1と2を与えると3がreturnされることを検証しています。

describe('addNumbers', function() {
  it('2 つの数値が加算できる', function() {
    chai.assert.strictEqual(window.addNumbers(1, 2), 3);
  });
});

js_testing/js_testing_0001.html を作成する

今回は、ブラウザでこのhtmlファイルを開くとテストの実行結果が表示されます。
例として以下の通りです。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--mochaによるテストの実行結果用cssを読み込み-->
<link rel="stylesheet" href="node_modules/mocha/mocha.css">
<link rel="shortcut icon" href="">
</head>
<body>

<!--mochaによるテストの実行結果が出力されるdiv-->
<div id="mocha">
</div>

<!--mochaとchaiを読み込み-->
<script src="node_modules/chai/chai.js"></script>
<script src="node_modules/mocha/mocha.js"></script>

<script>
//BDDスタイルを使用するので設定
mocha.setup('bdd');
</script>

<!--自分が作っているwebアプリケーションのjsを読み込み-->
<script src="js/application.js"></script>

<!--テストコードを読み込み-->
<script src="spec/add_numbers.spec.js"></script>

<script>
// テストを実行
mocha.run();
</script>

</body>
</html>

テスト結果を見る

上記で作成したjs_testing/js_testing_0001.htmlをブラウザで開くとテストが実行されて、検証結果が表示されます。

検証は成功しました。

関数を壊して再度テストを実行してみる

リファクタリングしてみたら関数が壊れてしまった。という状況を想定します。
returnされる値がリファクタリングする前と変わってしまったことを検知します。

js_testing/js/application.js を以下のように編集します。

window.addNumbers = function(a, b) {
  return (a + b) + '';
};

もう一度テストを実行すると検証が失敗しました。
3ではなく'3'がreturnされている旨が表示されます。

関数を直します。
これでリファクタリングに安心感が出てきそうです。

vuex2.xその0005 namespaced:trueにしたmodule用のオブジェクト構造をひとつ定義して使いまわす

参考にさせて頂いたページ

はじめに

namespaced:trueにプロパティ設定したオブジェクトをstore.registerModule()で複数回登録します。

ひとつのmodule用オブジェクト、ひとつのcomponent用オブジェクトを使いまわしできるようにしてみます。

今回のバージョン

  • Vue.js v2.1.10
  • vuex v2.3.0

サンプルソースコード

以下の通りです。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
<link rel="shortcut icon" href="">
</head>
<body>

<div id="app">
  <div>
    <counter :namespace="'firstCounter'" />
  </div>
  <div>
    <counter :namespace="'secondCounter'" />
  </div>
</div>

<script src="js/lodash.js"></script>
<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>
<script>
const components = {};
const store = new Vuex.Store({
  strict: true,
});

/**
 * counter
 */
(function(){
  const counter = {
    strict: true,
    namespaced: true,
    state(){
      return {
        count: 0
      }
    },
    mutations: {
      increment: state => state.count++ ,
      decrement: state => state.count-- ,
    },
    getters: {
      counterState: state => state ,
    }
  };

  store.registerModule('firstCounter', counter);
  store.registerModule('secondCounter', counter);

  components.counter = {
    props: ['namespace'],
    template: `
      <div>
        <div>{{counterState}}</div>
        <button @click="increment">+</button>
        <button @click="decrement">-</button>
      </div>
    `,
    methods: {
      increment() {
        this.$store.commit(this.namespace + '/increment');
      },
      decrement() {
        this.$store.commit(this.namespace + '/decrement');
      }
    },
    computed: {
      counterState() {
        return this.$store.getters[this.namespace + '/counterState'];
      }
    },
  };

})();

/**
 * VueModel
 */
const app = new Vue({
  el: '#app',
  store,
  components,
});
</script>

</body>
</html>
  • もっと上手い方法は無いものかと思いつつ、コミットするときのnamespaceはpropsで渡しています。
  • computedについてはmapState()やmapGetters()などを使って上手く書く方法がわからず、this.$store.gettersを使っています。

vuex2.xその0004 modulesによるstoreの分割と、mapGetters()でmoduleを跨いだgettersやstateの参照をしやすくする

参考にさせて頂いたページ

はじめに

babelは使用せずに、Chromeで動作確認をしています。 参考ページのmapGetters()のサンプルで、オブジェクトのスプレッド演算子(Spread Operator)が使用されていますが、エラーが出たのでObject.assign()を使用しています。

storeがすごく大きくなったりしないようにとか、機能ごとに分割する方が管理がしやすいだろうということでmodulesとかmapGetters()の使い方を学習しました。

今回のバージョン

  • Vue.js v2.1.10
  • vuex v2.3.0

サンプルソースコード

以下の通りです。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
<link rel="shortcut icon" href="">
</head>
<body>

<div id="app">
</div>

<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>
<script>
const mapGetters = Vuex.mapGetters;

/**
 * Store
 */
const moduleA = {
  strict: true,
  state: {
    count: 0
  },
  mutations: {
    incrementA: state => state.count++ ,
    decrementA: state => state.count-- ,
  },
  getters: {
    moduleAState: state => state ,
  }
};

const store = new Vuex.Store({
  strict: true,
  modules: {
    a: moduleA,
  },
})

/**
 * RootComponent
 */
const RootComponent = {
  template: `
    <div>
      <div>mapState使用 : {{ moduleAState.count }}</div>
      <div>this.$state.getters.moduleAStateの方 : {{ count }}</div>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </div>
  `,
  methods: {
    increment() {
      this.$store.commit('incrementA')
    },
    decrement() {
      this.$store.commit('decrementA')
    }
  },
  computed: Object.assign(mapGetters([
      'moduleAState',
    ]),
    {
      count() {
        return this.$store.getters.moduleAState.count;
      }
    }
  ),
  created() {
    console.log('this.moduleAState.count : ' + this.moduleAState.count);
  }
};

/**
 * VueModel
 */
const app = new Vue({
  el: '#app',
  store,
  components: { RootComponent },
  template: `
    <div class="app">
      <root-component />
    </div>
  `,
});

</script>

</body>
</html>

modulesとmapGettersを使ってみて個人的要点

  • modulesに分割しても、this.$store.commit('hoge')で全てのコンポーネントから全てのmutationsが呼び出せる
  • actions、gettersについても同様にthis.$store.dispatch('hoge')this.$store.getters.hogeなどで全てのコンポーネントから呼び出せる
  • this.$store.getters.moduleAState.countと記述すると冗長ですが、mapGetters()を使用するとそのコンポーネントのcomputedのgetterとして簡潔な記述で呼び出せるようになる

公式ドキュメントに書いてありますが、グローバルな空間にactionsなどもろもろの名前が定義されるので、どのコンポーネントからでもthis.$storeのプロパティとして呼び出せます。 namespaced: trueを設定すると名前空間を切ることができます。

CLIP STUDIO PAINT DEBUT その0001 タブレットPCで線の入り抜きができない問題を解消する raytrektab DG-D08IWP

参考にさせて頂いたページ

解決したいこと

ドスパラで raytrektab DG-D08IWP を購入して、CLIP STUDIO PAINT DEBUT が付属していたので使ってみたら線の入り抜きができない設定でした。

機種にかかわらずタブレットPCだと同様の事象が発生するようですね。

解決方法

私は以下の方法で解決できました。

  1. [ファイル]メニュー → [環境設定]を選択します。
  2. [タブレット]のカテゴリを選択します。
  3. [TabletPC]にチェックを入れてOKします。

非常に基本的かつ重要なことなのに、意外と必要な情報にたどり着けませんでしたが公式ページで解決されていました。

ついでに調整の方法

上記で入り抜きができるようになったら以下のページにある手順で細かな調整ができます。

vuex2.xその0003 vuex入門的な感じでToDo管理アプリケーションを作る

参考にさせて頂いたページ

はじめに

APIにリクエストする非同期な処理を想定して、actionsからcommitしています。 実際にAPIとか作ったらもう少し違う実装が適切かもしれないです。

ES2015が使えない環境下でのことを考慮するなど個人的事情からlodashを使用していたり、とはいえ全体的にはES2015で書いていますがそこはそっとスルーでお願いします。

今回のバージョン

  • Vue.js v2.1.10
  • vuex v2.3.0

サンプルソースコード

例として以下の通りです。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
<link rel="shortcut icon" href="">
</head>
<body>

<style>
.is-done {
  color: #999999;
  text-decoration: line-through;
}
</style>

<h1>タスク管理アプリケーション</h1>
<div id="app">
</div>

<script src="js/lodash.js"></script>
<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>
<script>
/**
 * store
 */
const store = new Vuex.Store({
  strict: true,
  state: {
    latestItemId: null,
    items: []
  },
  getters: {

  },
  actions: {
    addItem({commit, rootState}, content) {
      // 実際はここでAPIにリクエストしてDBのレコード追加が成功したらcommit
      const newItemId = rootState.latestItemId + 1;
      commit('addItem', {id: newItemId, content: content, isDone: false});
    },
    toggleItem({commit, rootState}, id) {
      // 実際はここでAPIにリクエストしてDBの当該レコードの更新が成功したらcommit
      const index = _.findIndex(rootState.items, function(o) { return o.id == id; });
      commit('toggleItem', index);
    },
    deleteItem({commit}, id) {
      // 実際はここでAPIにリクエストしてDBの当該レコードの削除が成功したらcommit
      commit('deleteItem', id);
    },
    getItems({commit}) {
      // 実際はここでAPIにリクエストしてDBからタスクリストを取得します
      const items = [
        {id: 0, content: '起きる', isDone: false},
        {id: 1, content: '朝食を食う', isDone: false},
        {id: 2, content: '昼食を食う', isDone: true},
        {id: 4, content: '夕食を食う', isDone: true},
        {id: 5, content: '風呂に入る', isDone: false},
        {id: 10, content: '寝る', isDone: false},
      ];
      const latestItemId = items[items.length - 1].id;
      commit('getItems', {latestItemId: latestItemId, items: items});
    }
  },
  mutations: {
    addItem(state, newItem) {
      state.latestItemId = newItem.id;
      state.items.push(newItem);
    },
    toggleItem(state, index) {
      state.items[index].isDone = !state.items[index].isDone;
    },
    deleteItem(state, id) {
      state.items = _.filter(state.items, function(item) {
        return item.id !== id;
      });
    },
    getItems(state, newState) {
      _.assign(state, newState);
    }
  }
});

/**
 * addItem
 * タスク新規追加フィールドのコンポーネント
 */
const addItem = {
  data() {
    return {
      content: '',
    }
  },
  template: `
    <section>
      <h2>タスクを新しく追加</h2>
      <div>
        <input type="text" v-model="content">
        <input type="submit" value="追加" @click="addItem">
      </div>
    </section>
  `,
  methods: {
    addItem() {
      if (!this.content) return;
      this.$store.dispatch('addItem', this.content);
      this.content = '';
    }
  }
};

/**
 * item
 * タスク一つ分のliタグのコンポーネント
 */
const item = {
  props: ['item'],
  template: `
    <li>
      <span :class="{'is-done': isDone}">id {{item.id}} : {{item.content}}</span>
      <button @click="toggleItem" v-text="buttonLabel"></button>
      <button @click="deleteItem">削除する</button>
    </li>
  `,
  computed: {
    id() {
      return this.item.id;
    },
    isDone() {
      return this.item.isDone;
    },
    buttonLabel() {
      return this.item.isDone ? '未完了に戻す' : '完了にする';
    },
  },
  methods: {
    toggleItem() {
      this.$store.dispatch('toggleItem', this.id);
    },
    deleteItem() {
      if (!confirm("削除しますか?")) return;
      this.$store.dispatch('deleteItem', this.id);
    },
  }
};

/**
 * itemList
 * タスク一覧のulタグのコンポーネント
 */
const itemList = {
  components: {item},
  template: `
    <section>
      <h2>登録されているタスク</h2>
      <ul>
        <item v-for="item in items" v-bind:item="item"></item>
      </ul>
    </section>
  `,
  computed: {
    items() {
      return this.$store.state.items;
    }
  },
  created() {
    this.$store.dispatch('getItems');
  }
};

/**
 * app
 * rootのコンポーネント
 */
const app = new Vue({
  el: '#app',
  store,
  components: {
    addItem,
    itemList,
  },
  template: `
    <div class="app">
      <add-item />
      <item-list />
    </div>
  `
});
</script>

</body>
</html>

Vuexとは

Storeを提供し、以下のようなルールに則った開発をすることでコードの見通しがよくなります。

  • Storeが持っているstateはmutationsによってのみ更新される
  • mutaionsは同期的でなくてはならず、非同期な処理はactionsが担う

正確な情報は公式ドキュメントを参照してください。

個人的まとめ

あくまでも個人的なもので、ES2015などを使用しない環境下のせいもあると思いますが、参考になればと思います。

  • VuexはStoreを提供するもので、コンポーネントは普通にVue.js単体で使っているときと同じ要領で書く
  • rootコンポーネントにstoreオプションを設定すると、子コンポーネントthis.$storeが参照できるようになる
  • コンポーネント内でstoreを参照するときはthis.$storeを使う
  • Storeはstrict: true,を設定した方がいい
  • Storeが持っているstateはmutationsによってのみ更新される
  • mutaionsは同期的でなくてはならず、非同期な処理はactionsが担う
  • mutationsが持っているメソッドは第一引数にstateを受け取る
  • actionsが持っているメソッドは第一引数にcontextまたは分割で{state, rootState, commit, dispatch, getters}を受け取る
  • mutaionを呼ぶときはcommitで、actionを呼ぶときはdispatch
  • getters, modules, plugins というキーもあるけど今回は使っておらず、いずれも大きい規模になるほど有用そう
  • actionのdispatchとpromiseについては「アクション · GitBook

Vuexに限らずVue.jsのコンポーネントのこと

  • コンポーネント内において、Storeが持っているstateの更新が反映されてほしい箇所はcomputedでreturnする
  • コンポーネント内において、親からもらったpropsの更新が反映されてほしい箇所は直接templateに書くか、computedでreturnする
  • アロー関数を使うとthisがコンポーネント自身を参照しないので理解して使う

vuex2.xその0002 mapStateのこと

参考にさせて頂いたページ

はじめに

storeを複数使うような規模のものを書いていないのでmapStateはまだ自分には必要なさそうですが、少し書いてみました。

サンプルソースコード

こんな感じかなー。と書いてみました。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
<link rel="shortcut icon" href="">
</head>
<body>

<div id="app">
</div>

<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>
<script>
const mapState = Vuex.mapState;

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: function(state){
      return state.count++;
    },
    decrement: function(state){
      return state.count--;
    }
  }
});

const Counter = {
  template: `
    <div>
      <div>{{ count }}</div>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </div>
  `,
  computed: mapState({
    // アロー関数は、コードをとても簡潔にできます!
    count: state => state.count,
    // 文字列を渡すことは、`state => state.count` と同じです
    countAlias: 'count',
    // `this` からローカルステートを参照するときは、通常の関数を使わなければいけません
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  }),
  methods: {
    increment () {
      this.$store.commit('increment')
    },
    decrement () {
        this.$store.commit('decrement')
    }
  }
};

const app = new Vue({
  el: '#app',
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
});
</script>

</body>
</html>

このコードについて

上記のサンプルでは、公式ドキュメントにあったオブジェクトスプレッド演算子は使っていませんが、ローカルなcomputedも定義する場合はそのような記述が必要になるようです。

オブジェクトスプレッド演算子が使えない環境下では、Object.assign()とかlodashの_.assign()とか使う感じでしょうか。