ジンジャー研究室

長めのつぶやき。難しいことは書きません。

puppeteer + express + mocha で快適 TDD している話

TDD という用語を使うとテストおじさんがやってきて、それはそうじゃないとか色々言い出すと思うんだけど、それが趣旨ではないので勘弁して欲しい。予防線ここまで。

Puppeteer でテスト

Puppeteer が世間的にも個人的にもブームだ。ヘッドレス Chrome を操ってクローリングしたりスクリーンショットを撮ったり色々出来る。

github.com

で、あれこれと遊んでいるうちにテストに使えるんじゃね?ということに気づいたので実践してみたら快適だったという話。ブラウザ操作してテストというのは昔から Selenium というのがあり、こちらはクロスブラウザで出来たりするんだけどまあ大掛かりでだるさを感じてしまう。メリットデメリットの比較はさておき、どうせならナウいやつを使ってみたい。よし使おう。

何をテストするか

普段から画面を見ながら開発しているので、どこに何が表示されているべきというテストはあんまりやる気が起きない。まあマイナーな画面だと知らず知らずのうちに表示されなくなってたりするんだけど、大体の画面は目で見て壊れてるのがすぐに目につくし、テストが通っていたところで見た目が整っていることは結局目で確認することになるので一向にモチベーションが上がらない。

だけど、そんな中にも「テスト書かなきゃ…」というのがあって、何かと言うと更新系 API を正しく叩けているかというやつ。画面はポチポチやってるとなんとなく動いているような気がするんだが、実は裏で大変なことになってたりする。

  • 実はデータを送信していない
  • バリデーションエラーがあるのにデータを送信している
  • 1文字入力する毎にデータを送信している
  • フォームの入力項目のうち一部しか送信していない
  • 更新前のデータを送信している
  • 別の API を叩いている

アホみたいだが、実際に上のは全部踏んだ。しかも、画面に現れないので気づくのが遅れる。 というわけで真っ先になんとかしよう。

仕組み

前提としては、サーバーサイドが JSON を返す REST API で、そのモックサーバーが Express.js (Node.js) で書かれている。これを改造して、Puppeteer で入力したデータが正しく Express に届いたかを調べられるようにする。

Express で API を記述するときは大抵 app.get('/foo/:id', (req, res) => { ... }) のように書いていくので、この get とか post にフックを仕掛けてAPI にどういうデータが届いたかを全て記録する

app = wrap(app); // 色々仕込む

app.get ...
app.post ...
...

そうすると何回かアクセスすると

state = {
  'GET /foo/:id': [ req1, req2, req3, ... ],
  'POST /foo/:id': [ req1, req2 ... ],
};

こういうオブジェクトが手に入るので、ここにアサーションをかける。幸いにして Puppeteer と Express は同じ Node.js の同一プロセス上で動かせるので、サーバー側で作られたこのオブジェクトは Puppeteer 側でそのまま手に入る。

で、あとはこれを mocha でテストできるようにする。大体次のような感じのテストになる。

describe('ArticleEdit', function() {
    beforeEach(async () => {
      await goto('/#/article/0');
    });
    it('should send title', async () => {
      await inputText('#article-title', 'foo');
      // 1 回のアクセス
      assertTimes('PATCH /articles/:articleId', 1);
      // 0 番目の body の title が 'foo'
      assertBody('PATCH /articles/:articleId', 0, ['title'], "foo");
    });
});

アサーション関数が洗練されてないのは認める。

mocha に一工夫する

mocha はまともなテストライブラリなので before とか after とかが書ける。工夫次第でどんどん快適になる。

before(全体の)

  • サーバーのセットアップ
  • Puppeteer のセットアップ

before(各ページの)

  • 目的のページにアクセスして操作可能な UI を一覧表示する (a, button, input, select とそれらの id )

beforeEach

  • サーバーの状態をリセット
  • localStorage (セッションなど)をリセット
  • 目的のページにアクセス

afterEach

after(全体の)

  • サーバーを終了
  • Puppeteer を終了

テスト駆動にする

ここまで来たら、せっかくなので画面ができる前にテストを書いてしまう。デザインカンプがあると大体どういう UI があるのかイメージしやすいので、「この URL にアクセスしてこの ID の要素を操作したらサーバー側にこれが届くだろう」が書けてしまう。これで見た目がボロボロでもとりあえず一通り期待通りの動きをするページを、画面を見ずに作れる状態になる。

副作用としてよかったことは、要素の ID がいい感じに統一されたこと。今までは必要になった時点で「そういえば id 振らなきゃ名前どうしよ」みたいな行き当たりばったりをしていたが、こうすると仕様として先にビシッと決まるので統一感が取れて良い。

まとめ

というわけで、会社で使ってて今の所まあ快適にできている(テスト対象のコードは Elm )。

できれば既存のライブラリを探しても見つからないから OSS 化したいんだけど、要求のバリエーションが無限にありすぎて個人ではとてもメンテできなそう。「作ったよ」と言いたかったが、しんどいのが目に見えてるから仕方なくブログで方法だけ紹介することにした。誰かすごいひと、作って!

CSS in Elm 方式を導入してから1年半以上たった感想

CSS in JS(Elm)したら想像以上に良かった という記事をずいぶん前に出して結構ブクマを貰えたんだけど、今は「なんだかなー」と思っているので整理する。冷静に考えると、そもそもこのエントリで書いていることのほとんどが「良かった」ではなく「悪くなかった」としか言ってない。

class が無いのでどのスタイルを直せば良いのか分からない

これが一番大きい。このビューのスタイルを直したい、と思った時に class が書いてないのでどこを直しにいけば良いのか分からない。

DevTools であれこれ試す時に同じスタイルが一気に変わらない

class でやれば同じところが変わってくれっる

DevTools からコピペできない

あれこれ試した後「これだ」と思ったらそこからコピペした後、 Elm のコードに直さないといけない。

スタイルを変えるのにコンパイルが必要

Sass とか Post CSS をすでに導入していたら同じ条件なので関係ないが、何かの都合で見た目ををサクッと変えたい時に CSS をいじって解決したくなる。

CSS が進化した

今は変数も使えるしグリッドも使えるし、ブラウザも追いついてきたので相対的に Elm でロジックを書く必要性が減った。スコープももう少ししたら Shadow DOM とか使ってどうにかなるのではないか。

関連アプリと共通化出来ない(仮説)

これは実際に困ったことではなく「そういうことが想定される」というケースなのでまだ仮説段階。例えば隣のアプリは React を使っているが見た目は統一したいとか、徐々に JS から Elm (あるいは逆)に置き換えたいといった場合に、CSS を共通基盤に使うことはあると思う。

引継ぎが厳しい(仮説)

今回は引き継いだ人は普通に出来る開発者だったのでそれほど問題なかったが、もしデザイナーとかだったら厳しかったかもしれない。

というわけで、今は「普通に CSS ファイルで良くね?」に傾いているけど、それでまた問題になったらその時はまた考える。

Promise でリトライや同時実行数の制御をするやつ作った

f:id:jinjor:20170917145732p:plain

JavaScript でバッチ実行を少しでも楽にしたいという思いで作ってみた。

www.npmjs.com github.com

まだバージョン 0.7.0 だけど、大体のことは出来るはず。

機能

デモ を触ると大体何ができるかわかると思います。

  • 実行間隔の指定
  • 同時実行数の制限
  • リトライ数の指定
  • リトライ間隔の調整
  • 失敗したリクエストの返却

動機

DynamoDB への書き込み時に流量を制御しようと色々していて、 Promise が不便だと思った。SDK でもなんか色々パラメータがあると後から知ったのだけど、まぁ Promise が便利になるのに越したことはないということで。もちろん既存のライブラリも沢山探したけど、しっくりくるのが無かった => 作ろう。

API に関しては、シリアライズ可能なデータとしてのリクエストの配列を受け取るようにして、失敗した時に情報を保存したり渡したりできるようにしている。リクエストの数はそんなに多くない想定なので、ストリームで読みつつ書き込むみたいな処理は今の所ない。

今後

やる気が持続すれば付くかもしれない機能。

  • バックオフのもう少し緻密な設定
  • タイムアウトの設定
  • ログ埋め込みの仕組み
  • 入力バリデーションエラー

それでは良い Promise ライフを!

Elm のパイプ |> の良さ

小ネタ。

JavaScript

[1,2,3].map(a => a + 1)

が、 Elm だと

[1,2,3]
  |> List.map (\a -> a + 1)

で、両方とも左から右に読めるからそんなに変わらないなーと思ってたんだけど、一つ違う点に気づいた。JavaScript で Promise を気持ちよく連鎖してて書いてて、いざ並列実行しようとなった時に

const promises =
  [1,2,3].map(a => a + 1).map(toPromise)
Promise.all(promises)

のように少し回りくどくなり、なぜ promises.all() と書けないのかと考えたら「配列にメソッドを追加するのが微妙だから」と気づいた(prototype 拡張で不可能ではない)。一般的に言うと既存の型に何か関数を追加できない。

Elm のパイプを使う場合、その制約はなくて

[1,2,3]
  |> List.map (\a -> a + 1)
  |> Debug.log "converted" -- List に対して Debug モジュールの関数を使う
  |> List.map toString
  |> MyListUtil.getByIndex 1 -- List に対して MyListUtil モジュールの関数を使う
  |> Debug.log "result" -- Maybe に対して Debug モジュールの関数を使う

こうしてどんどん連鎖できる。便利。

と言うのを、 JavaScript と Elm を行ったり来たりしてて気づいた。おわり。

Elm 用の CSV デコーダーを作った

Elm 界隈では構文解析するライブラリを「パーサー(Parser)」、それを Elm の型に落とし込むものを「デコーダー(Decoder)」と呼び分けることが多い。パーサーは既にあって(lovasoa/elm-csv)、デコーダーで良いものがなかったので作った。

CsvDecode - elm-csv-decode 1.0.0

使い方

elm-tools/parser インスパイアのパイプライン式。

-- CSV の各行をこの User 型にしたい
type alias User =
    { id : String
    , name : String
    , age : Int
    , mail : Maybe String
    }


-- デコーダー Decoder User を作る
userDecoder : Decoder User
userDecoder =
    succeed User
        |= field "id"
        |= field "name"
        |= int (field "age")
        |= optional (field "mail")


-- デコードしたい CSV を用意
source : String
source =
    """
id,name,age,mail
1,John Smith,20,john@example.com
2,Jane Smith,19,
"""


-- 実行
> CsvDecode.run userDecoder source
Ok
    [ { id = "1", name = "John Smith", age = 20, mail = Just "john@example.com" }
    , { id = "2", name = "Jane Smith", age = 19, mail = Nothing }
    ]

API 一覧

型を見るだけで使い方が本当に分かるのか実験。

-- Types
type Decoder a
type alias Options = { separator : String, noHeader : Bool }

-- Primitives
succeed : a -> Decoder a
fail : String -> Decoder a
field : String -> Decoder String
index : Int -> Decoder String
fieldsAfter : String -> Decoder (Dict String String)
fieldsBetween : String -> String -> Decoder (Dict String String)

-- Convertion
int : Decoder String -> Decoder Int
float : Decoder String -> Decoder Float
string : Decoder String -> Decoder String
optional : Decoder a -> Decoder (Maybe a)

-- Transform
(|=) : Decoder (a -> b) -> Decoder a -> Decoder b
map : (a -> b) -> Decoder a -> Decoder b
andThen : (a -> Decoder b) -> Decoder a -> Decoder b

-- Run
run : Decoder a -> String -> Result String (List a)
runWithOptions : Options -> Decoder a -> String -> Result String (List a)
runAll : Decoder a -> String -> (List a, List String)
runAllWithOptions : Options -> Decoder a -> String -> (List a, List String)
defaultOptions : Options

分からないと思うのでドキュメント読んでください。

苦労した点

空文字の扱い。要するに foo,,bar の2列目。特に optional を使った時に、数値だと Nothing なのに文字列だと Just "" になるのは微妙なので、デフォルトで空文字は null 扱いにして string と指定した場合のみ値が存在することにした。

ソース

割ときれいに書けたかもしれない。

elm-test は最近のアップデートでトップレベルに Test 型の関数を置くだけで勝手に実行してくれるようになった。便利。

Chrome 拡張機能「JSON-YAML Toggle」を作った

f:id:jinjor:20170722154842p:plain

chrome.google.com

これは何?

JSONYAML をページ上で切り替えるツール。最近 AWS を触っていて、設定ファイルを JSON でも YAML でも書けるんだけどサンプルがどっちか片方しかなく、コピペに不便だった。

使い方は、コンテテクストメニューで「Toggle JSON/YAML」を選ぶだけ。

f:id:jinjor:20170722154953g:plain

ソースはこちら。

github.com

似たものを作りたい方へ

「右クリックしたら画面を操作する」Chrome 拡張を作る場合、上のリポジトリを参考にすると楽。

  1. パッケージとして公開にするには ZIP 化が必要なので、package/ をそのフォルダにする。
  2. manifest.json に必要事項を書く。必要なファイルとか実行条件とか。
  3. context menu 登録用の background.js と、各ページで動作する content-script.js を用意。右クリックされたら前者から後者にメッセージが送られる。DOM は後者からのみ操作できる。
  4. アイコン用画像を3種類ほど用意する。
  5. package を zip 化して Chrome ストアにアップロードして、概要など必要事項を入力する。宣伝用の画像とスクリーンショットが追加で必要。確定すると数分後に公開される。

今回は webpack で js-yaml モジュールと一緒にバンドルしたが、リンクで読み込むのでもいけるような気がしないでもない。

過去に作ったもの

実は似たものを過去にも作っていて、こちらは「日英分割」という日本語と英語の間にスペースを挿入する Chrome 拡張。使い方は上のと同じ。

f:id:jinjor:20170722161901p:plain

chrome.google.com

github.com

JSON-YAML Toggle はここからコピーして作ったので、ほとんど調べずに2日で作れた。

Elm を既存の JavaScript と併せて使う N の方法

今月、社外向けに1回、社内向けに1回 Elm の紹介をしたところ、両方とも好評で「ぜひ使ってみたい」という声が多く、良かったと思う一方で、ここは補足すべきだったなーという点があったので書きます。

両者に共通して頻出だったのが、

  • 既存プロジェクトの一部に導入するにはどうすればいいか
  • JavaScript のライブラリは使えるのか

という質問。 Elm の紹介としてはメインの話題じゃないんだけど、まぁ実際に導入しようと思ったら確かにそこ一番気になると思う。

Elm の初期化方法

まず、Elm の仕組みから見ていくと、例えば Main モジュールをエントリポイントとした場合、JavaScript 側で最も簡単に初期化する方法は次の通り。

<script src="/assets/elm.js">
<script>
Elm.Main.fullscreen();
</script>

fullscreen() を使うと、Elm で書いた画面がウインドウいっぱいに表示される。さらに、Elm の main 関数はフラグ(引数)を受け取ることができるので、設定の類を最初に突っ込むと良い。

<script>
Elm.Main.fullscreen({
  apiHost: 'https://hogehogeapi.com'
});
</script>

次に、画面の一部に Elm の画面を埋め込むには、次の例のように embed() を使う。

<div id="target"></div>
<script>
var elemenet = document.getElementById('target');
Elm.Main.embed(elemenet);
</script>

ちなみに、ここまでの例では HTML に直接ロジックを書いているが、最近のフロントエンド開発では Node.js で開発することが多い。Elm が吐き出すコードはそのまま Node.js のモジュールとして読み込める

var Elm = require('./elm.js');

Elm.Main.fullscreen();

さらに、 React コンポーネントとして Elm を利用することもできる。公式サポートではないが数十行のコードでそういう仕組みが作れる。具体的な話は公式記事の How to Use Elm at Work に書かれている。

JavaScript のライブラリを使いたい

Elm から JavaScript を呼ぶ(あるいはその逆)をするために Port という仕組みがある。これを使うと、JavaScript の世界と Elm の世界はお互いにデータを通信し合うことができる。

ここで注意すべき点は、 JavaScript は Elm の下位レイヤーではなく外部サービスになる ということ。Elm の世界から見た JavaScript の世界はサーバーだと思えばいい。これを公式ドキュメントではJavaScript as a Service」と呼んでいる。

例えば、 JavaScript 側にスペルチェックのロジックが実装されていて、 Elm 側からそれを使いたいとする。その場合、Elm 側から「スペルチェックをしてくれ」というリクエストを投げる。すると JavaScript は処理結果を Elm 側に返す。コードにするとこう。

var app = Elm.Spelling.fullscreen();

app.ports.check.subscribe(function(word) {// Elm 側からデータを受け取る
   var suggestions = spellCheck(word);
   app.ports.suggestions.send(suggestions);// Elm 側に結果を返す
});

制約としては、Elm 側のコードは非同期処理の書き方になる。この辺りは無理してハックするよりも、そういうものだと受け入れてそれ前提で設計を組むのが良いと思う。クラウドの画像処理エンジンを利用するのと同じ。テキストエディタとか(まだ Elm のライブラリがない)も無理せずに JavaScript のライブラリを使って Port でやりとりする。

Elm の世界と JavaScript の世界は隔離されていて、 Port 以外の方法でやりとりすることはできない。言い換えると、Elm が JavaScript のライブラリを「ラップ」することは出来ない。もし、ラップが可能であるとすると Elm の特徴である「実行時例外が発生しない」という保証を守ることができない。ユーザーからしたら見た目 Elm の関数だから、実は中で JavaScript がモリモリ使われていてクラッシュするという話だと疑心暗鬼になってしまう。だからそれは出来ないようになっている。(Elm の外側で起きたらそれは知らない、500エラーのようなもの)

同様に、Elm のエコシステム(外部ライブラリ)も JavaScript ライブラリをラップしているものは一切なく、全て Elm で書かれている。JavaScript をラップしたライブラリを公開することはシステムに禁止されている。だからインストールして使ってみたらクラッシュするという心配はない。

CSS はどうするの?

Elm 固有の特別な仕組みはない。普通に class をつけて 外部の CSS ファイルでスタイリングすれば良いと思う。

他の方法として、このブログでは一度 CSS in Elm (CSS in JS の Elm 版、要はインラインスタイル)という方法を紹介していてモジュール化の観点からは良いのだが、開発ツールとか色々考えていくと総合的なバランスは悪いので、一般的用途にはあまりおすすめできない。elm-css という有名なライブラリもあるのだが個人的にはちょっとやり過ぎ感を感じていて保留中。あとは CSS Modules の Elm 版を作る動きもあったが、今どうなってるか知らない。

とまあ、高度なことをしようとすると色々事情はあるものの、普通の CSS を普通に書くぶんには何も困らない。

失敗しない Elm の導入方法

良いツールがあったとしても、上手くいくかどうかは最終的にチームワークとか組織論とかに行き着いてしまう。社内向けには「ハイリスク・ハイリターン」と表現した。JavaScript に比べたら全然情報がないので、スタート地点でリスク高いのは否定できない事実で、そこからどう工夫してリスクを下げていくかが戦略になる。

その辺の話も公式記事の How to Use Elm at Work に書かれている。この記事にかかれているのは、 Elm を導入して成功した事例の共通点をまとめたものだ。短い記事だから興味のある人は全部読んでほしいのだが、まとめるとこう。

  • 小さい部分から実験しながら徐々に導入する
  • チームに必ず一人めっちゃ詳しい人がいる状態にする
  • 最初のプロジェクトは一番メリットの出る場所を戦略的に選ぶ
  • 議論も良いがとにかく Elm のコードを実際に書いてみる

本文にも書いてある通り 「リスクを最小化する」 のがポイント(というか、これ全然 Elm に限った話じゃないと思う)。

まとめ

なんやかんや言って、コードを見るのが一番イメージ湧きませんか?なので連携部分をコードで紹介しました。参考になれば幸いです。

追伸: Elm 本は 0.19 が出たタイミング(時期未定)で校正入れるので、もう少しお待ち下さい。