ジンジャー研究室

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

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 化したいんだけど、要求のバリエーションが無限にありすぎて個人ではとてもメンテできなそう。「作ったよ」と言いたかったが、しんどいのが目に見えてるから仕方なくブログで方法だけ紹介することにした。誰かすごいひと、作って!