トップ 最新 追記

2018-06-03 [長年日記]

_ flowのClass Typesよいかも 

そういや、まだ使ってなかった。(なぜか string, number くらいしか使ってなかった。)なんとなく RubyKaigi の型TL眺めてて思い出した。

Class Types | Flow

そもそもプロパティの宣言が必要

前からだっけ? v 0.73.0 で確認。

// @flow
class Receiver {
  constructor(obj) {
    this.sender = obj
  }
}

class Sender {}

class Differ {}

new Receiver(new Differ())

まずこの状態だと property sender is missing と怒られる。

incompatible

で、property 宣言を足すと、

// @flow
class Receiver {
  sender: Sender

  constructor(obj) {
    this.sender = obj
  }
}

class Sender {}

class Differ {}

new Receiver(new Differ())

以下のように怒られる。

Cannot assign obj to this.sender because Differ [1] is incompatible with Sender

class が違うので意図通り。

継承は見てくれる

// @flow
class Receiver {
  sender: Sender

  constructor(obj) {
    this.sender = obj
  }
}

class Sender {}

class Differ extends Sender {}

new Receiver(new Differ())

No errors!

よしよし。

まぁ「そもそも引数の型で見てくれ」という意見もありそうだけど。(別に引数には絶対に書かないぞという宗派ではないけど、できれば書きたくはない。)

まとめ

なんでこんな初歩的なことを確認してるかというと、その昔試した PHP の Type Hinting が継承したクラスを認めないという仕様だったため、それ以来 Type Hinting に対してすごくネガティブな気持ちを持っていたんだけど、まぁ普通こうだよなと確認して安心しているわけです。(もしかしたら今は違うのかな? もう試したいとも思ってないんだけど)

Tags: JavaScript

2018-06-04 [長年日記]

_ Google Cloud Functions Emulator動かしてみた

なんかいろいろあって Google Cloud Functions 試そうと思って、まずは Emulator 使ってみようと思ったんだけど、試すというよりは動かすまでで力尽きたのでそのメモ。

--local-pathオプションが消えててハマった

なんかプロジェクトっぽくディレクトリを切って作業する場合、普通はカレントディレクトリにいきなりコード置かないよね。で、その状態だとどうしても deploy できなくて、いろいろ試した結果、

--local-path という option は beta から消えてる

ことが分かりましたとさ。

1.0.0-beta.1 (#177) · GoogleCloudPlatform/cloud-functions-emulator@04ec0b0

なるほど。deploy できるようになって確認すると、マジで急に local-path という「オプション」だけが消えてて、functions list と打った時に出てくる情報としてはやはり Local path のままなので、明確なポリシーじゃないのかな?

この変更は2月に入ってるのにドキュメントが追いついてないし、2018-06-04 時点でまだ Functios Emulator は世界的にも使われてないのか? option が変わるって割と大きな変更だと思うんだけど、話題を見ない。

とりあえず Google には Feedback しといたので、そのうちドキュメントかコードかどっちか分からないけど直るんじゃないかな。

Functionの名前をexportsしろ

Functions のサンプルコードって、こんななんだけど、

exports.helloHttp = (req, res) => {
  res.send(`Hello ${req.body.name || 'World'}!`);
};

Node.js のコードは基本的に書いてこなかったので、exports で名前を明示するのを見る機会が全然なく、なんだこれと思ったわけ。*1

で、試してみると名前を明確に exports しないと

$ ./node_modules/.bin/functions deploy hello --trigger-http
ERROR: Error: Node.js module defined by file index.js is expected to export function named hello

という感じで怒られる。

deploy したい function の名前と exports する名前を合わせておけ

というルールらしい。なるほど。

global installはzshと相性が悪い

これは単純にこういう話

$ which functions
functions: shell built-in command

functions-emulator もあるのでそっちが使えるんだけど、すげー長い。じゃあ alias かというと話なんだけど、そこまでやらないといけないってのもちょっとなぁという感じ。

*1 require する側が自由に名前を決められるのがメリットなんだと思ってた。


2018-06-05 [長年日記]

_ フロントエンドのrequireはテストを考えると注意が必要

先日導入した Nuxt の環境で mocha-webpack でテストコードを書いている。

なんだけど、require / import 周りでどうしても伝統的な環境のクセが出てハマることが何回か起きていたのでそのメモ。

非webpack環境下でファイルを扱うサンプル

例えばファイルを扱うコードを書く際に、従来(webpack じゃない環境)はよく以下のようなコードを書いていた。

class Klass {
  /**
   * @return {String}
   */
  path() {
  }

  /**
   * @return {String}
   */
  load() {
    return <require とか read とか>(this.path())
  }
}

ファイルの扱いを気にしたくないので、ファイルの情報とその内容、データ構造などをセットで class の責務として持たせ、Klass を使う側は欲しい情報だけを適度に扱うメソッド経由で触る。

で、テスト時に path() を stub out してテスト用のファイルを食わせる。HTTP request だと request 先の URI を環境に応じて切り替えるような感じ。stub out じゃなくて外からパス情報を与えろとかあるかもしれないけど、まぁいずれにせよ「パス情報だけを独立して扱える」という前提なわけだ。

ところがこのようなコードは webpack ではいくつかの意味でダメなコードなのだ。

  1. そもそも webpack で動く require には変数も与えられない
  2. webpack の require は parse 時に即実行される

そもそもwebpackで動くrequireには変数は与えられない

これはメソッドとして分離しているかどうかではなく、以下のように動的にパスを組み立てるコードでも一緒。

const path = dir + base + param + '.js'
require(path)

これは「require が現れたらその中のパスを解釈していい具合に読み込んで JS の中に JS やデータを置く」という webpack の挙動に対して、「パスの解釈を許さない」というコードになってしまうのだ。

require の中に動的な組み立てが入っている場合は解釈できるので、どうしてもやりたい場合はそのように書くとよいが、その場合もテストには向いていない。

webpackのrequireはparse時に即実行される

プロダクトコードに require を書き、テストコードで stub out した require を書いた場合、実は require そのものが2回起きる。

まぁそらそうかという感じではあるんだけど、要は webpack を通す段階で require の登場する部分は遅延評価できない。人間の意識としてはコードが実行される前、 webpack がコードの変換を行う段階で require は実行、解決されてしまう。

まとめ

だから stub out するかどうかではなく、テスト対象のコードの require は都合よく変更できないので、以下のようにしてあげるのが正解。

  1. パス情報の解釈、require の処理は外で行い、
  2. 外からデータを与え、
  3. データの解釈だけを責務としてあげる

require ではなく HTTP になっていれば問題ないが、今回はそこまでやる必要はないので webpack の require で食わせるデータをいい具合にビルドプロセス*1で生成してやろうと思っていたら、require 周りのテストで思ったように動かずに悩んでしまった、という話でした。

*1 こっちは webpack ではなく Node.js


2018-06-08 [長年日記]

_ Vuex PersistedStateの値の復帰のタイミングが謎だったのでNuxtのplugin書いて解決した

背景

先日から Vue (Nuxt) + Vuex + Vuex PersistedSate でモノを動かしてるんだけど、

なんかふいにデータが矛盾して動作がおかしくなる

ことが起きていた。

まぁアタリはついていて、Nuxtでrouteに応じてVuex Storeをmodule分割する方法 - あーありがち(2018-04-19) にある動的に module 構造の Vuex Store を生成している処理と、 Vuex PersistedState を通じて localStorage からデータが返ってくる処理がそれぞれバラバラに動いていることで、「Vuex Store の値を見て判定する処理」がタイミング次第の微妙なシロモノになっているんだろうなぁと思ってたんだけど、決定的な解決策が見つからないでいた。

通常の使い方だと問題にならないけどテストしてるとよく踏み抜くので、ごまかしごまかしやっていたのだが、今回解決できたので記録しておく。

※ ちなみに今回の話は Vuex PersistedState が localStorage の値でメモリ上のデータを書き換え終わったら Vuex がそれを通知してくれて Vue コンポーネントの computed に反映できれば、実はあまり悩まなくて済んだのだけど、対策としてやったこと自体は Vuex PersistedState を使っていなくても参考になるし、今後自分で参考にしたいので書いてます。

使っているもの

おさらい - Nuxtを利用したVueアプリにきっとありがちなこと -

  1. Vuex PersistedState 経由で localStorage から値が復帰するのは mounted() 以降
  2. this.$route や this.$store に依存するコードが安定して動くのも mounted() 以降
  3. Nuxt では基本的にアプリの全体像を気にしなくても済むように Page component を突っ込むと routing も自動解決する、逆に言うとアプリ全体を組み立てるプロセスに介入するのは難しい

これらが合わさると何が起きるかというと、

Page component の mounted() が複雑で長大な一つのメソッドになりやすく、しかもタイミング問題まで抱え込んでしまう。

これ割と地獄感あるのですよ。

解決方法

Vuex Storeの初期化はpluginで行う

plugin function からは

  • 初期化済みの store オブジェクト
  • page component の有無から自動で解決される routes オブジェクト

の両方にアクセスすることができる。したがって、Nuxtでrouteに応じてVuex Storeをmodule分割する方法 - あーありがち(2018-04-19) でやったように

this.$store.registerModule(NAME, OBJECT)

ではなく、plugin の中で

export default ({app, store} => {
  store.registerModule(NAME, OBJECT)
})

みたいにすると、Vue の root component が初期化される前にすべてを揃えることができる。

mounted()の中でsetTimeout

Vuex Store に localStorage から値が返ってくるのは mounted() 以降なのだが、値が返ってきてからでないとメモリ上の値で reset することはできない。

ということで以下のような感じになる。

mounted() {
  setTimeout(() => {
    resetの処理({値1, 値2, ..})
  }, 0)
}

あれ、結局 mounted() の中が複雑なのは一緒じゃね?と思うかもしれないが、mounted() の中で registerModule すると、localStorage から値が返ってくるタイミングも遅くなり、かなり制御が難しくなるので、

mounted より前に registerModule しておいてからの setTimeout

という扱いにしておくのがよい。また、setTimeout() の中も直接長いコードを書かずに、遅延させてるのが分かる reset 処理だけ独立させた PureJS な function として切り出して、そいつに外からバンバン値を与えてやるのがよさそう。なんなら this.$route とか this.$store とかバンバン与える。

※ なんで reset したいのかはアプリによると思うが、保存しておいた何かと比較して違ってたら何か処理を行いたい、という要求は普通によくあると思う。

まとめ

Nuxt + Vue アプリを書く際には、

  • Vue component ではない形で解決できるものはないか考え、どんどん分離する
  • plugin は root component 初期化前に async/await で動作するのでよいぞ

を覚えておくのがよさそう。

特に、意外と View に関わらないコードは多くなるので、積極的に Vue component 以外の形にして、Nuxt のディレクトリレイアウトで言う components/ つまりレールから外れた場所に置くコードを増やしてやるのが吉っぽい。

です!

*1

参考

*1 今回、対処方法を見つけるまでの試行錯誤はそれなりにありましたが、見つかってからサクッと Vuex Store の組み立て、リセット処理の位置(実行タイミング)を分離、変更できたのは、もちろんできあがった Store の動作と Vue component からのアクセスについてもテストコードを書いてあったからです。


2018-06-11 [長年日記]

_ Vue.jsチョット書いたのでふりかえり

ちまちま各論のメモは起こしてあるけど、ざっくり全体のメモがなかったので、自分が現時点で意識していることをまとめてみた。

Vue.jsのメリット

Single File Componentが標準で扱える

Single File Component がすべての問題を解決するとは思っていないのだけれど、まず自分は

  • jQuery 的な DOM 要素とそのイベント中心の書き方
  • Unobstrusive JavaScript

の組み合わせは「操作」が「要素」と密結合しているにも関わらず、コードとしては非常に距離が遠く凝集度が低すぎると考えている。HTML と JavaScript の紐付けは class や ID で行うが、ページをまたいで影響させるのは難易度が高すぎる。またコードがイベント中心かつ操作中心になりすぎてしまい、抽象度が低すぎる。本当に考えるべき状態やロジックまで意識を到達させるのはとても難しい。要するにものすごくちぐはぐなのだ。

この問題を Single File Component は比較的うまく解決していると思う。機能として意味のある HTML 断片をコンポーネントとして切り出し、その隣に実際の機能を記述することができる。また Scoped CSS を書けるのもよい。

これが Vue.js では追加設定なしに書ける。

ある程度オールインワンなので準備が楽

AngularJS (v1) のように世界が閉じすぎておらず、React や Mithril のように単機能すぎない。一応 Babel などのビルドプロセスがなくても動かすことができる。Angular (2+) のように TypeScript を新たに勉強する必要もない。

この準備が複雑すぎないことはとても重要と考えている。少なくとも現在の環境では

  • デザイナーなど必ずしも JavaScript に明るいわけではない人にも扱える
  • Node.js ヘビーな JavaScript の環境構築に詳しくない人でも扱える

ことを重視している。

環境構築は目的ではなく手段。手段の習得がヘビーすぎるのは完全に本末転倒と言ってよい。準備が楽であればあるほど新しく参加するメンバーの初期コストが下がる。それは特に小さく固定的でないチームにとって重要である。

必要な用語が少ない

正確な理解とより良いコードを書くためには、当然のことながら各機能の意味や役割を表現するために用語を使いこなす必要はあるのだが、さしあたって動くコードを書くためにあまり多くの用語を必要としないのはよいことである。

(※ ただし、準備が楽で必要な用語が少ないからと言って必ずしも学習コストが小さいとは言い切れないと思っているので、このような表現になっている。)

ある程度流行っている

個人的には、JavaScript を利用したシンプルな ViewModel フレームワークとしては Mithril が好きなのだけれど、いかんせん CSS フレームワーク、UI フレームワークと組み合わせるとか、単純にノウハウなどの流通において React, Vue.js, Angular に大きく水を開けられてしまっているのは事実である。

こうなると他のツールと同じことを再現しようとした時にはややコストの大きな環境になってしまいやすい。Mithril 自体の準備はそんなに大変じゃないけど、デザイナーが手を加えていくプロセスにおいてゼロベースのコーディングの量が増えてしまいやすかったり、何か意図通りに動かず困った事態に陥っても、とにかく自分で試行錯誤する以外の解決方法が見つからない可能性もある。

これがライブラリのアップデートに煩わされず、細かいデザイン要件のない業務向けなら CSS の優先度を下げて妥協するといった判断もできるが、Consumer 向けの Web を扱おうと思うとさすがにそういうわけにはいかないだろう。

Vue.js なら十分に流行っているのでそのような心配はほとんどないと言ってよい。安心してググれるし、情報交換が可能である。

PWAやネイティブアプリへの道もある

これらはさすがに標準対応じゃないけど、道があるという事実だけメモしておく。

現在のVueとの付き合い方

  • ノーフレームワーク(一部Babelだけアリ)
  • Rails + Webpacker
  • Nuxt(SSR 付き)

をやってみた*1 が、この中でバランス的には Rails + Webpacker で小さなアプリから始めるのがよさそうだと感じている。要するにサーバ側との統合作業も Webpack などの JavaScript 周りのごちゃごちゃとした設定作業も必要にならないもの、である。

逆にサーバ側と絶対に密結合にならないようにするために Nuxt で全部縛っちゃうというのもギプスとしてはアリかなという気がしている。少なくともこれが使いこなせればサーバ側の選択肢は広がる*2。ただし Nuxt は SSR がすごい悩む。最初は SPA モードのみにするのもアリだとは思う。*3

とりあえずサラから Webpack を書くのはやらないと決めている。このこだわりは正解だったと思っている。*4

Vueの扱いで気をつけるべきこと

Vue Componentに過度の期待をしない

Vue Component で扱えること、Vue Component に書けることは限られている。基本的には

  • 変更を監視するデータ
  • (ユーザの入力に対する)イベントハンドラ
  • VM Life Cycle Hook

くらいしかない。あとは変更を監視するのがデータそのものではなくメモ化機能付きの function か、extends 元があるか、くらいだ。

データバインディングはとても便利だ。変更の操作を記述する必要がない。しかしそれ以上は面倒を見てくれない。結局のところ人間の「考える」余地は大きく、設計がダメならコードが必要以上の複雑さを抱えてしまうのは普通のプログラミングと同じである。現実世界の課題は Vue Component というか React が集中しようとした ViewModel なコンポーネントで扱う範囲より広く、より複雑な形で存在している。

Vue Componentの外のコードを活用する

これはアセットバンドラがないとダメなので準備が楽という話と矛盾してしまうが、Vue Component の外にコードが置けることは重要である。

上にも挙げたが、Vue Component の中で function の書ける場所は基本的には computed か methods しかない。ごく単純に言い換えるとデータバインディング用かイベントハンドラ用である。

また Vue Component 同士で function を融通する方法は extends か mixin、要するに mixin である。雑に言うとフラットに function をぶちまけるだけで名前空間的にはぶつかりまくるし、Component から独立した function を融通する方法はないということである。

これはもう function をいい具合に管理することは Component のスコープから外したと考えるしかない。JavaScript が持っている機能は JavaScript の方で解決しましょう、普通に export / import しましょうということだ。Vue Component 以外を export / import すれば普通に class も利用できるし、Component から独立した共通の function を名前の重複を気にせず import することもできる。

VirtualDOMで扱えない世界や一般的なモデリングなどの手法に精通する

結局のところ現実世界の課題を分析し、適切なコードを書くには

  • HTML 5 時代の新しい要素である video や audio, canvas
  • Vue Component の外の Pure JS な世界
  • OOP や DDD などのパラダイム

に精通する必要があると言ってよい。

ViewModel コンポーネントが解決するのはあくまでごく一部なのだ。

Lifecycle Hookは用法、用量を守って

Lifecycle Hook は便利なのでついついいろんなことをさせてしまうが、Lifecycle Hook の中のコードが長くなると

  • function に分割されていないのでコードの意図を読み取りにくい
  • タイミングを制御しきれない場合がある
  • テストコードを書く際の stub out の単位がでかすぎる

といった問題が出てくる。これは歴史の中に学ことができる。昔なつかしい「コンストラクタ頑張りすぎ問題」だ。

Lifecycle Hook の中のコードを整理するためにも Vue Component の外にコードを分割して置くのが恐らくかなり重要なテクニックになると言ってよいと思う。

*1 素振りとしては、素から、vue-cli から、Parcel からも試したが、実プロダクトとしては上の 3 つになっている。

*2 単に API の役割を果たしてくれればコードベースを共有する必要もまったくない。

*3 自分はなんか逃げたくなかったのでやらなかったけど

*4 結局テストコードのために書くことになるんだけど


2018-06-15 [長年日記]

_ async/awaitやPromiseで気をつけること

ES2017のasync/awaitのキソ練習 - あーありがち(2018-05-26) のあとにがっつりハマっていたのでそこで得た知見をメモ。

asyncの効果は各functionブロックで切れるので逐一指定が必要

例えば

async foo() {
  return new Promise((resolve, reject) => {
    ..
  })
}

ってコードがあるじゃろ? この Promise の中では await できんのじゃ。正解はこうじゃ。

async foo() {
  return new Promise(async (resolve, reject) => {
    ..
  })
}

Promiseではちゃんとrejectする、rejectしてすぐreturnする

async foo() {
  return new Promise(async (resolve, reject) => {
    const bar = await this.bar()

    if ( !bar ) {
      return reject(new Error('message'))
    }

    resolve(bar)
  })
}

async function は Promise を返すので、通常の function のように値を返すことはできない。成功も返せないし失敗も返せない。

失敗は必ず reject() で教える必要がある。

そしてもう一つ、reject() は単なる function call であり、それが終わったら次に処理が進んでしまう。ちゃんと return して foo() を抜けなければいけない。

resolve ばかりを考えていると UnhandledPromiseRejectionWarning の警告が出まくることになる。ちゃんと reject してあげよう、そしてすぐ抜けよう。

async/awaitで待つコードを閉じ込める

async foo() {
  return new Promise(async (resolve, reject) => {
    let c = {}

    this.bar().forEach((e) => {
      c[e] = await this.baz(e)
    })
  })
}

みたいな、同期処理の中に非同期処理が紛れ込んでいてさらに全体のブロックの中で副作用ベースで変数を組み立てる、みたいなやつは大変あぶない。

async foo() {
  return new Promise(async (resolve, reject) => {
    // 非同期処理のカタマリ

    // 同期処理のカタマリ
  })
}

に分離しよう。

Promise.allで全部待て

上のコードはもっと言うとループの1回目は待つかもしれないけど全部は待ってくれないコードになり得る。そこで実際にやるとしたら Promise.all() を使って以下のような感じになる。

async foo() {
  return new Promise(async (resolve, reject) => {
    // 非同期処理のカタマリ
    const result = await Promise.all(this.bar().map(async (e) => {
      return ..
    })

    // 同期処理のカタマリ
    result.forEach((e) => {
      ..
    })

    ..
  })
}

ここに上で述べた resolve, reject を加えると以下のような感じがいったん完成形と言えそう。

async foo() {
  return new Promise(async (resolve, reject) => {
    // 非同期処理のカタマリ
    const result = await Promise.all(this.bar().map(async (e) => {
      return .. await this.baz() ..
    })

    // 同期処理のカタマリ
    let c = {}

    result.forEach((e) => {
      const [key, value] = e

      .. // 同期処理だけなら副作用ベースでも従来通り大丈夫

      if ( ) {
        return reject(new Error())
      }
    })

    resolve(c)
  })
}
Tags: JavaScript

2018-06-24 [長年日記]

_ RSpec 3.5のRequest Spec使ってみた

おじさんのためのRSpecとRequest Specの歴史ふり返り

  • RSpec にはもともと Request Spec はあった
  • Capybara 1.x 時代は Request Spec + Capybara という組み合わせはアリだったが、Capybara 2.x 時代には Feature Spec と組み合わせてくれという話になり、そっちが流行った
  • Feature Spec の登場は RSpec 2.14
  • この頃同時に RSpec 3 へ向けて should が expect なるぞーなど RSpec に疲れた人が増える(脱線)
  • RSpec 3.5 で Request Spec は以前に比べて高速化したのを期に Controller Spec じゃなくて Request Spec オススメだよ宣言

使ってみた感想

  • いくら速くなったとは言え、Controller Spec よりは明らかに重い
  • ただし多少の遅さは我慢のうえで Request Spec を使うメリットはある
    1. view の render が自動で走るので response body も何の前準備もなしに取得できる
    2. Feature Spec ではないので Capybara の語彙に縛られずにいきなり Post できる

ごく簡単に言うと まず API を叩いて response を確認するといったことが普通にできる。地味かもしれないけどこれが助かるシーンは確かに今後増えそうだ。


2018-06-29 [長年日記]

_ Webpacker 3.2.0がすべてをfile-loaderで飲み込む問題から設定の覗き方を知る

※ この問題は 3.2.1 で修正済みです

Webpacker はデフォルトの設定だけで結構いろんなことができる*1のだが、逆に何かの設定を追加しようとする際に何が原因でうまくいかないのかよく分からないという事態に陥ることがある。

今回ハマったのは yaml-loader を追加しようとしたがどうしても yaml-loader に処理が渡らないという問題。

結論から言うと Webpacker 3.2.0 が aggressive すぎる。

Excluding vs testing files &#183; Issue #1127 &#183; rails/webpacker

これは 3.2.1 で直ってるんだけど、逃げ方はあって、

config/webpacker/environment.js で最後の

module.exports = environment

より前に

const fileLoader = environment.loaders.get('file')
fileLoader.exclude = new RegExp(
  fileLoader.exclude.toString().replace(/^\//, '').
                                replace(/\$\//, '').
                                replace(/\$/, '|ya?ml$'))

ってやると特定の loader(今回は file-loader )の設定を書き換えることができる。

なるほど、Webpacker はある程度のマジカルさを受け入れる代わりに便利になるんだけど、一応 Webpack の設定の中身を扱うことはできるってことがわかった。

*1 少なくともメジャーなフレームワークなどは


2018-06-30 [長年日記]

_ Webpacker環境へのJest + Power-Assertの導入が本当に簡単だった話

まとめ

  • Jest は確かに設定減ってる。でも zero config は言いすぎ*1
  • Webpack の設定をテスト用に書き足す必要はなかったが、Jest 用の transform を追加するが必要あり、Webpack の再発明感はある*2
  • Power-Assert が Babel に対応してて設定が楽になってきてる
  • Jest よりも Webpacker のクセが強い(これはまた別に書く)

背景

以前 Rails 4.2にWebpacker入れた - あーありがち(2018-01-19) で Webpacker を導入したわけだが、やはりテストがなくてつらかったので*3、今回はテストの環境を Jest で作った話。

実は最近も Nuxtを利用したVueアプリのユニットテストに関するメモ - あーありがち(2018-04-24) で似たような話をやっているんだけど、その時は

  • mocha-webpack
  • テスト環境用のwebpack.config.js
  • jsdom-global
  • power-assert

をちまちまと準備していった。勉強にはなったけど、またアレやるのはちょっとイヤだなと思っていたので、最近評判のよい Jest を試してみることにした。

Jest &#183; Delightful JavaScript Testing

まず最小限で動かす

yarn add --dev jest とか npm install --save-dev jest とかでインストール。

で、 package.json に

"test": "jest --config <config-path>"

を書く。Rails + RSpec 環境なので、場所は

spec/javascripts/jest.conf.js

がよいだろう。

で、jest.conf.js の中身だが、基本的には

module.exports = {
  rootDir: ".",
  modulePaths: <webpacker-path>
  ..

で動く。

rootDir ( または roots ) や modulePaths はこの jest.conf.js からの相対パスで指定するようだ。conf を独立させずに package.json の中に書く場合は package.json からの相対パスになる。

これでテストコード側からは modulePaths で指定したパス以下のプロダクトコードを気軽に読み込むことができる。はずだ。ただし Jest は Node.js で動いており、2018-07 時点では当たり前だがそのままでは import は使えないので、Babel の登場を待つ必要がある。

Mochaの追加とjsdom-globalの設定は不要

自分が Mocha にこだわっていたのは Jasmine では power-assert を使うのが難しいと思っていたためだったのだが、Jest では Jasmine2 + jsdom-global の環境が最初からできあがっており、またこの状態で describe / it 形式でテストコードは書けるので、ここは本当によかった。いきなり

describe('', () => {
  it('', () => {
    console.log('foo')
  })
})

が動く。

jsdom-global はここまでの流れでは関係ないが、Vue component のテストを書く際に jsdom 関係の設定は一切不要だった。

余談だけど Jest では assert を呼ばなくても true が返ってなければエラーになるっぽい。こういうの、いいな。

やはりimportは解釈できないのでtransformの追加が必要

Vue.jsでアプリを書いている場合、基本的にはこれでいける。

transform: {
  "\\.js$": "babel-jest",
  "\\.vue$": "vue-jest",

もちろん babel-jest, vue-jest はインストールしておくこと。

この辺が非常に Webpack の再発明っぽくてどうなんだと思のだが、babel-jest, vue-jest については特に設定は不要なので助かる*4。babel-jest は Jest本家が、vue-jest は Vue本家がサポートしているので安心。

YAML でデータを食わせている場合にも yaml-jest を追加設定すればよいだけ。

以前は JavaScript のテストでは fixture をどうするのかが課題の一つだったが、イマドキはもうこれでよいような気がする。違う? 適当にテストデータを YAML で用意すればいいんじゃないかなぁ。

Webpacker環境独自の課題

ここまでは順調だが、Webpacker 環境の場合はまだ注意が必要。

まず、Webpacker の作った .babelrc の presets env が modules: false になっているので、babel-jest を設定しているにも関わらず import できない。そこでこれを消そうかと思うのだが、影響がよく分からないのと、ついでにやることがあるので、 .babelrc で

"env": {
  "test": {
    ..
  }
}

を用意して環境を切り替えることにする。いちばんシンプルには以下のようになる。

"env": {
  "test": {
    "presets": ["env"]
  }
}

Power-Assertの導入

実は Webpacker の課題を解決するのと同時に Power-Assert を使うために上では .babelrc で env を追加してある。

yarn add --dev power-assert babel-preset-power-assert

しておいて、先ほどの env.test の中に以下のように追加する。

"env": {
  "test": {
    "presets": ["env", "power-assert"]
  }
}

これでテストコードの中で

import assert from 'power-assert'

して使うことができる。

よーしこれでバンバンTDDできるってもんだ。

*1 jsdom-global は確かに zero config

*2 でも出力しなくて済むので、Webpack と Jest は load と transform 周りを切り離すと本当はいいんだろうなぁ

*3 どーして毎回こういう話になるのかというと、自分が新規案件をゼロから書く機会を奪われている(集中すると他に手が回らなくなって困る)ので、こういう話が増えるのです。とは言え今回は 2016年にやっていたことに比べればへなちょこ級に楽ちんです。 http://aligach.net/diary/20161228.html#p01

*4 特に vue-jest が Webpack にも Babel にも依存していなくてどちらにも何も設定しなくて済むのは助かる。Webpacker 側も無設定なので本当に助かる。