kitak's blog

Kみたいなエンジニアになりたいブログ

2019年3月4日

鹿児島3日目。足が棒のようになっていて痛い。

最終日は観光をする。まち巡りバスに乗って、城山、西郷洞窟、仙巌園に行った。雨の日で景色はちょっと微妙だったけど、桜島の大きさに感嘆する。 ちょっと駆け足で3時間程度で回った。

お昼は、とんかつ川久に行こうとしたが、行列がすごく一時間待つということだったので夕方に行くことにして、鹿児島中央駅構内のお寿司を食べた。アラやキンメダイなど、白身のお魚をいただく。ねっとりと甘い味でよかった。

身体を休めるために夕方まで天文館近くのスパでのんびりする。足をマッサージしてもらった。痛気持ちいいかんじで、声を出すのをなぜか必死でこらえてしまった。 サウナがフィンランド式っぽい本格的なやつで、ロウリュウのサービスがあると書いてあり期待したが、時間になってもロウリュウは始まらなかった。残念。

鹿児島中央駅に戻って、とんかつ川久へ。17時の開店10分前についたが、既に3,4人並んでいた。開店と同時にぞろぞろ人がやってきてあっという間に席が埋まる。上黒豚ロースカツをいただく。トンカツでこんなレアっぽいのあるの!とびっくりした。箸を入れると脂が流れて、慌てて口に運ぶと口いっぱいに旨味が広がって、うほぉ、となった。とんかつをいつでも食えるぐらいの人間であり続けたい。

とんかつをいただいた後はバスで空港へ。本を読みながら、ぼんやりしていた。飛行機のトラブルで到着が遅れて、家についたのは日付が変わった後。
録画しておいたビジャレアルvsアラベスを見る。前半が終わったぐらいのところで疲れて寝た。

2019年3月3日

鹿児島マラソンに参加。天気はレース中は曇りで、絶好のマラソン日和だった。
4時間半が目標で、最初はゆっくりのペースで始めて、15km毎にペースを上げていくプランだったが、35km地点で急に足が重くなり、勾配のアップダウンが続いたこともあって、ペースダウン。 最終的に4時間44分21秒でフィニッシュ。 直前に30kmLSDもやったのでいけるかなーと思ったけど、甘くなかった。40~50kmのLSDをやるか、長距離ランの最後にビルドアップ走を加えて、終盤にスパートをかけることができる足を作らないとだめだなーという反省。

ラソンの後は、ホテルに戻って次の予定まで休息。

18時から前職の同期のNと、アミュプラザの「いちにぃさん」でご飯を食べる。蒸ししゃぶしゃぶが野菜がホクホク、豚肉の旨味が染みていておいしかった。

2019年3月2日

鹿児島マラソンに参加するために鹿児島へ。

早朝に足に刺激を与えるために6:30~7:00/kmぐらいのペースで5kmを走る。 その後、飛行機で羽田空港から鹿児島空港へ。鹿児島は雨。鹿児島空港から鹿児島中央駅へバスで移動する。大体40分くらい。

移動中「親切すぎるiPhoneアプリ開発の本」を読んでいた。コードをいじくって実験をしながらSDKの解説をしている本で、簡単なチュートリアルをこなした後に読むと自分の中でブラックボックスになっていたアプリの起動の過程とかUIKitのクラスヒエラルキーが頭の中で整理されてよかった。逆に何も知らない状態で読むと、きつそう。

ホテルにチェックインした後に鹿児島中央駅から市電で天文館へ。公園でナンバーカードを受け取る。雨のせいで露天や催し物はガランとしていた。
友人のIさんと落ち合って、むじゃきでしろくまを食べる。

移動で疲れてしまったので、ホテルに戻って、2時間ぐらい横になる。明日がクラシコなので、WOWOWでやっていたクラシコ直前特集(契約なしで見れる)をなんとなく見ていた。鹿児島にいるので、リアルタイムで見れないのが残念。

その後、普段、仕事をしていて、今回マラソンに一緒に参加する福岡の会社のOさん、Hさんと鹿児島中央駅近くの吾愛人で豚しゃぶを食べる。脂が甘くておいしかった。

明日は7時集合なので、早めに寝る。

2019年2月28日

有休を取った。

3日後の鹿児島マラソンに備えて、3kmを5:00/kmのペースで走って足に刺激を与える。若干、足が硬い感じもしたけれど、途中、気がついたら 4:27/km のペースを楽に走っていて、いいかんじだな、と思った。

走った後は近所のコーヒー屋で店員と雑談。その後、部屋の掃除をしたり、鹿児島行きの旅のしおりを作ったりしていた。
当日は雨が降ることがほぼ確実なので、雨対策にポンチョ、ワセリン、ランニングキャップを購入。翌日に届けてくれる Amazon 便利。

TypeScript で p5js のスケッチを書くための雛形を作った

Nature of Code -Processingではじめる自然現象のシミュレーション-

Nature of Code -Processingではじめる自然現象のシミュレーション-

を読みながら、手を動かすために作った。

なんで p5js ?

個人的にとっつきやすいから。

なんで TypeScript ?

VSCode で補完が効くのと、凡ミスを防ぐため。最近は、特に理由がなければ基本的に TypeScript で書いている。

FuseBox を使う

ビルド環境の構築に時間をかけたくなかったので、なるだけ設定を書かずに済むモジュールバンドラを探した。最低限、コンテンツの配信(ウェブサーバー)と、ファイルを変更したらビルド&リロードしてくれたらOK 。 いろいろ探したら、FuseBox がよさそうだった。TypeScript ファーストなのが決め手。

設定はこんなかんじ。

const { FuseBox, WebIndexPlugin } = require("fuse-box");

const fuse = FuseBox.init({
  homeDir: "src",
  output: "dist/$name.js",
  plugins: [
      WebIndexPlugin({path: "."})
  ]
});

fuse.bundle('app')
    .instructions(`> sketch.ts`)
    .watch()
    .hmr({reload: true});

fuse.dev();

fuse.run();

node fuse で起動できる。必要に応じて、npm scripts に移動する。

スケッチ( src/sketch.ts )

こんなかんじ。draw に必要なコードを書き足していけばOK。 バンドルの過程で、setupdraw が関数スコープに入るので、明示的に window オブジェクトのプロパティとして生やす必要がある。

/// <reference types="p5/global" />

import 'p5';

function setup() {
  createCanvas(720, 720);
  noCursor();
}

function draw() {
  clear();
  ellipse(50, 50, 80, 80);
}

window['setup'] = setup;
window['draw'] = draw;

Puppeteer でページを自動操作するスクリプトを書く時に使うスニペット

画面が複数あったり、フォームの項目が多い場合に毎回最初から実行して試行錯誤するのは大変。以下のようなスニペットで、 REPL 環境を用意することで、DevTools でセレクタを調べたり、動きを確かめつつ、スクリプトに書き足していく。

const puppeteer = require('puppeteer');
const readline = require('readline-promise');

const rlp = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: true
});

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    devtools: true,
  });
  await page.goto('https://example.com');
  while (true) {
    try {
      const statement = await rlp.questionAsync('> ');
      const result = await eval(statement);
      console.log(result);
      if (statement.trim().indexOf('browser.close()') === 0) {
        break;
      }
    } catch (e) {
      console.error(e);
    }
  }
})();

ブラウザが閉じられたか判定する方法がよく分からなかったのでお茶を濁したかんじになった。

Nuxt.js で Vuex の結合テストを書く方法を考えた

この間、友人と焼き肉を食べていて、「Nuxt.js で Vuex の結合テスト書くのどうやってますか?」という話になったので、考えてみました。

Vuex の結合テストの定義

この記事では、アクションやミューテーション単体ではなく、 アクションをディスパッチした結果のステートやゲッターが意図通りになっているか確認するテストのことを、Vuex の結合テストと呼びます。

方針

Vuex の結合テストで課題となるのは、テスト対象の Vuex Store インスタンスの生成です。

今回は、(少し強引ですが)Nuxt.js のドキュメントのエンドツーエンドテストのサンプル( 開発ツール - Nuxt.js )をベースに Store インスタンスの生成を Nuxt.js に任せることにしました。

最初、モジュールモードの場合に Nuxt.js が内部でおこなっている Store オプションの組み立て( nuxt.js/store.js at e0cc9a1cc6ffdfc87f944d0669ec1dd8d35130c9 · nuxt/nuxt.js · GitHub )をテストコードで再現しようと思いましたが、込み入った処理なのと、アクションのコードで利用している Nuxt.js のプラグインや Webpack を利用した環境変数などの仕組みの再現も考える必要があり、コストが高いと考えて諦めました。

また、Store インスタンスの生成を Nuxt.js に任せることで、(スタブ・モック化はしますが)実際に動作するコードに限りなく近い対象をテストすることが可能になります。

手順

  • テストを実行するためのコンポーネントを作成する
  • Nuxt コンストラクタのオプションの extendRoutes を用いて、上記のコンポーネントレンダリングするルーティングの設定を追加する
  • テストでスタブ・モック化したい処理、例えば、action 内でおこなっている通信やストレージを扱う処理は、事前に Nuxt.js プラグインにしておき、テストでは必要に応じてスタブ・モックに差し替える
  • renderRoute で上記のルートを呼び出し、テストの関数をコンポーネントfetch フックで実行する

サンプルコード

コードで具体的にみていきます。

ストアのコード

テスト対象の小さいストアのコードです。記事をDBから取得して、ステートにセットします。

export const state = () => ({
  entry: null,
});

export const mutations = {
  setEntry(state, doc) {
    state.entry = doc;
  }
};

export const actions = {
  async fetchEntry({commit}, entryId) {
    const doc = await this.$db.get(`entry_${entryId}`).catch(() => null);
    commit('setEntry', doc);
  }
};

DBのプラグインのコード

DB へのアクセスは、Nuxt.js プラグインによって注入されたオブジェクト(this.$db)を通して行います。データベースは、なんでもよいのですが、このサンプルでは PouchDB ということにします。プラグインの内容を以下に示します。

import PouchDB from 'pouchdb';

export default (_, inject) => {
  const db = new PouchDB('http://localhost:5984/entries');
  inject('db', db);
};

テストに関係するコード

テストの関数を実行するコンポーネント

テストコードから渡された関数を、Store のインスタンスやスタブ・モックオブジェクトに差し替えるための関数(inject)を引数として呼び出すための Page コンポーネントです。 pages 配下に設置するとテスト実行時以外にもアクセスできてしまうので、test ディレクトリ配下に execute-test.vue という名前で保存します。

<template>
  <p>Component for testing</p>
</template>

<script>
export default {
  async fetch(context) {
    const {app, req} = context;
    try {
      await req.test(context, app.$inject);
    } catch (e) {
      throw e;
    }
  }
};
</script>

injector プラグイン

Nuxt.js プラグインで注入されたオブジェクトをテストコードで、スタブ・モックのオブジェクトに差し替えるためのプラグインを用意します。plugins の配下に injector.js という名前で保存します。

export default (_, inject) => {
  inject('inject', inject);
};
ava を使ったテストコード

テストコードです。Nuxt.js のドキュメントのエンドツーエンドテストをベースにしているので、ava をつかっていますが、jest など他のテストフレームワークやテストランナーを使っても問題ありません。
解説するポイントが複数あるので、下で詳しく解説します。

import test from 'ava';
import sinon from 'sinon';
import { Nuxt, Builder } from 'nuxt';
import { resolve } from 'path';

let nuxt = null;

test.before('Init Nuxt.js', async t => {
  const rootDir = resolve(__dirname, '..');
  let config = {};
  try { config = require(resolve(rootDir, 'nuxt.config.js')) } catch (e) {};
  config.rootDir = rootDir; // project folder
  config.dev = false; // production build

  config.env = config.env || {};
  // テスト実行時か判断するための環境変数を設定する
  config.env.testEnabled = true;

  config.router = config.router || {};
  config.router = {
    ...config.router,
    extendRoutes (routes, resolve) {
      // テストを実行するためのルートを追加する
      routes.push({
        name: 'execute-test',
        path: '/test/execute',
        component: resolve(__dirname, 'execute-test.vue')
      });
    }
  };

  config.plugins = config.plugins || [];
  // プラグインで注入されたオブジェクトを差し替えるためのプラグイン
  config.plugins.push('~/plugins/injector.js');

  nuxt = new Nuxt(config);
  await new Builder(nuxt).build();
  nuxt.listen(4000, 'localhost');
});

test('プラグインで注入されたオブジェクトの一部のメソッドをスタブにする', async t => {
  const test = async (context, _) => {
    // DBへのアクセスをスタブ化
    const db = context.app.$db;
    const get = sinon.stub(db, 'get')
    get.returns(Promise.resolve({
      _id: "entry_kitak",
      _rev: "1-c507349e68734f039c71a5050a3cac67",
      content: "Hello!",
      title: "kitak",
    }));

    // actionのdispatchとstateの確認
    await context.store.dispatch('fetchEntry', 'kitak');
    t.is(context.store.state.entry.content, 'Hello!');
  };
  await nuxt.renderRoute('/test/execute', {
    req: {
      test,
    },
  });
});

test('プラグインで注入されたオブジェクトをスタブ・モックオブジェクトで注入しなおす', async t => {
  const test = async (context, inject) => {
    // DBへのアクセスをスタブ化
    const db = {
      get() {},
    };
    sinon.stub(db, 'get').callsFake(function () {
      return Promise.resolve({
        _id: "entry_kitak",
        _rev: "1-c507349e68734f039c71a5050a3cac67",
        content: "こんにちは!",
        title: "kitak",
      });
    });
    inject('db', db);

    // actionのdispatchとstateの確認
    await context.store.dispatch('fetchEntry', 'kitak');
    t.is(context.store.state.entry.content, 'こんにちは!');
  };
  await nuxt.renderRoute('/test/execute', {
    req: {
      test,
    },
  });
});

// Nuxt サーバーをクローズする
test.after('Closing server', t => {
  nuxt.close();
});

テストコードの解説と考慮すべきこと

設定オプションの変更

before フックでテスト対象のアプリケーションのビルドとサーバーの起動をおこなっています。ポイントはテストのために Nuxt コンストラクタの設定オプションを変更することです。

まず、環境変数testEnabled というプロパティを追加しています。

config.env = config.env || {};
// テスト実行時か判断するための環境変数を設定する
config.env.testEnabled = true;

今回は利用していませんが、テスト時に実行したくないコード、例えば、Vuex のプラグインでストレージに書き込んだり、他のサーバーにイベントを送信するといった処理を回避するために利用します。

2つ目がテストを実行するページコンポーネントにアクセスするためのルートの追加です。

config.router = config.router || {};
config.router = {
  ...config.router,
  extendRoutes (routes, resolve) {
    // テストを実行するためのルートを追加する
    routes.push({
      name: 'execute-test',
      path: '/test/execute',
      component: resolve(__dirname, 'execute-test.vue')
    });
  }
};

最後がプラグインで注入したオブジェクトをスタブ・モックのオブジェクトに差し替えるためのインジェクタープラグインの追加です。

config.plugins = config.plugins || [];
// プラグインで注入されたオブジェクトを注入し直すためのプラグイン
config.plugins.push('~/plugins/injector.js');

最初は、テストケースごとにスタブやモックに差し替える Nuxt.js プラグインを用意して、アプリケーションをビルドしようかと考えていましたが、ビルドに10~20秒程度時間がかかるので、ビルドは before フックでの一回のみにし、汎用的なインジェクタープラグインを用意することにしました。

Nuxt.js プラグインで注入されたオブジェクトのスタブ・モック化

今回のアプローチでは、Nuxt.js がビルドしたコードをテスト対象にしているので、proxyquire などの import 文にフックさせてスタブ・モックさせるライブラリは利用できません。
DB や ストレージにアクセスしたり、環境に応じて処理を分岐するコードを、 積極的に Nuxt.js プラグインにして、スタブ・モック化を図ります。
スタブのためのライブラリとして、今回、Sinon.js を使いました。

サンプルコードでは、ふたつテストケースがありますが、プラグインで注入されたDBにアクセスするオブジェクトをどうスタブするかが異なります。

1つ目はメソッドレベルでスタブに変更し、2つ目はスタブオブジェクトを作成し、injector プラグインで追加された関数を利用して、注入し直して差し替えています。
どちらの方法を使うかは状況に応じて、使い分けると良いでしょう。

スタブの後始末とテストの並列化について

今回のサンプルコードでは、スタブの後始末をおこなっていませんが、これは Nuxt.js がリクエスト毎にコンテキストを生成するためです。今回のテストに関係する箇所でいうと、リクエスト毎にプラグインの関数の呼び出し(dbオブジェクトの生成)と Store インスタンスの生成が行われます。
これはテストを実行する上で非常に有用な性質です。後始末の記述を不要にする他に、テストの並列実行を可能にします。

ただし、プラグインで、関数の外側で生成したオブジェクトを変数にキャッシュして、再利用しているような場合は、リクエストのコンテキストにまたがって同じ参照を共有するので、後始末が必要になります。
例えば、今回の DB のプラグインでは以下のようなコードが関数の外側で生成したオブジェクトを変数にキャッシュして再利用しているコードです。

import PouchDB from 'pouchdb';

const db = new PouchDB('http://localhost:5984/keywords');

export default (_, inject) => {
  inject('db', db);
};

後始末は、1つ目のテストケースの場合は db.get.restore() を呼び出し、 2つ目のテストケースの場合はオリジナルのオブジェクトを取っておいて終了時に注入し直します。※

const originalDB = context.app.$db;

// スタブ化やアクションのディスパッチなど
// ...

inject('db', originalDB);

また、ava はデフォルトでテストを並列で実行しますが、リクエストのコンテキストにまたがって同じ参照を共有している場合、スタブの処理が競合する可能性があります。この場合、テストは直列で実行する必要があるので注意が必要です。 ava で直列にテストを実行するには --serial オプションを指定します。

ava --serial

※ この処理を毎回書くのが大変な場合は injector を改良することをおすすめします。注入用の関数をラップした関数を用意し、その関数でオリジナルのオブジェクトを保存する処理を追加したり、保存しておいたオブジェクトに戻す関数を提供してもよいかもしれません。

アクションのディスパッチとステートの確認

ここはコードに書かれている通りなので、特に解説はおこないません。

テストの実行

テストを実行するパスに対してrenderRouteを呼び出します。

await nuxt.renderRoute('/test/execute', {
  req: {
    test,
  },
});

テストの関数は、reqオブジェクトのパラメータとして、コンポーネントではコンテキストから参照するようにします。

async fetch(context) {
  const {app, req} = context;
  try {
    await req.test(context, app.$inject);
  } catch (e) {
    throw e;
  }
}

まとめ

今回は Nuxt.js のドキュメントのエンドツーエンドテストのサンプルをベースに、Vuex の結合テストを記述・実行する方法を紹介しました。 Nuxt.js のプラグインの仕組みを使って、必要な箇所をスタブ・モック化しつつ、Nuxt.js がインスタンス化した Store を対象にテストを実施することが可能です。