ジンジャー研究室

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

個人的によく使う npm ライブラリを紹介してみる

偏ってます。

もっと有名なのは沢山あるけど、自分が普段よく使うのじゃないと紹介できないので。

argv

引数をパースするやつ。

chalk

色をつけるやつ。

dotenv

環境変数をファイルから読むやつ。

fs-extra

fs に欲しいけどないやつ。

watch

ファイルを監視してコマンドを実行するやつ。

nodemon

ファイルを監視してサーバーを再起動するやつ。

mocha

テスト。

prettier

フォーマッター。

puppeteer

ヘッドレス Chrome

express

サーバー。

passport

OAuth 。

typescript

TypeScript 。

elm

Elm 。

npm

Node.js 用のパッケージマネージャー。

puppeteer + express + mocha で快適 TDD している話(続編)

前回の記事の続き。

jinjor-labo.hatenablog.com

会社で開発中でオープンソース化していないテストツールがまたいくらか便利になったので進捗報告してみる。 ツールの概要は上の記事で説明しているけど、一言で言うと「mocha から puppeteer 叩いて express に届いたリクエストにアサーションをかける」ようなテストが楽に書ける。

前との差分

以下、前回より便利になったポイントを自慢していく。

APIカバレッジが取れるようになった

API が一度でも叩かれたかどうかを調べて網羅率を見る。 テストが一通り終わった後、こういうのが出る。

[ OK ] GET /foo/foo
[ OK ] PUT /foo/foo
[ OK ] GET /bar/:barId/bar
[ NG ] POST /bar/:barId/bar
covered 3/4

ページのカバレッジが取れるようになった

こちらはテストケースで訪問したページの網羅率を見る。 全体でどんなURLがあるかはサイト内をクローリングして調べる。

[ OK ] /#/
[ OK ] /#/config
[ OK ] /#/articles
[ NG ] /#/articles/(number)
covered 3/4

UI のカバレッジが取れるようになった

こちらはページ内の UI をどれだけ網羅したか。

/#/articles/(number)
[ OK ] #article-tilte-input
[ OK ] #article-body-input
[ OK ] #submit-button
[ NG ] #submenu-toggle
covered 3/4

テストケースに併せてモックデータを差し替え可能になった

今まで一枚岩のデータしか無かったのが、柔軟に適切なデータをサーバーから返してもらえるようになった。 これでログイン中のユーザーの権限を変えてみたり、ページネーションのテストために大量のデータを放り込める。

おかしな HTML 要素を警告するようになった

<a> なのに href が無いぞーだったり a11y 的によろしくないものを警告する。 まあこの辺は普通の E2E のツールにありそうな機能ではある。

記法が進化した

前回ちょっとダサかったアサーションpower-assert でいい感じになった。

    it("should send fields correctly", async () => {
      await inputText("#signup-email-address", "a@b.c");
      await inputText("#signup-password", "Passw0rd");
      await inputText("#signup-user-name", "A B");
      await $.click("#signup-submit");
      at("POST /auth/signup", results => {
        assert(results.length === 1);
        assert(results[0].body.email_address === "a@b.c");
        assert(results[0].body.password === "Passw0rd");
        assert(results[0].body.user_name === "A B");
      });
    });

TypeScript 化した

時代は TypeScript らしいですよ。

以上

応援してもらってオープンソース化と出社のモチベーションを高めたい。

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 型の関数を置くだけで勝手に実行してくれるようになった。便利。