bouzuya.hatenablog.com

ぼうずやのにっき

bouzuya/beater-snapshot 0.2.0 をつくった / その設計メモ

bouzuya/beater-snapshot 0.2.0 をつくった。 2019-11-19 の 0.1.0 の続き。

beater-snapshot は snapshot testing の機能を提供する npm package 。

0.2.0 では関数のインタフェースを変えて assert を含んだ形にしてしまった。

以下はなぜ変えたのかのメモ。

まず参考に Jest を見る。 expect(actual).toMatchSnapshot()actual が snapshot と match するかを確かめるものだと分かる。簡潔だ。

では beater-snapshot の 0.1.0 がどうか。

const expected = await snapshot('name', actual);
assert.deepStrictEqual(actual, expected);

まず snapshot() とは何なのか。これは actualname に書き込み or 読み込みして、できた snapshot を返す。

なぜ snapshot() が読み書きを兼ねないといけないのか。書き込みに nameactual の両方が要るのは分かる。しかし読み込みは name だけで良いはずだ。saveSnapshot('name', actual)loadSnapshot('name') ではダメなのか。

await saveSnapshot('name', actual);
const expected = await loadSnapshot('name');
assert.deepStrictEqual(actual, expected);

こう書くと今度は saveSnapshot() が必ずしも動かないことに違和感がある。

if (update) await saveSnapshot('name', actual);
const expected = await loadSnapshot('name');
assert.deepStrictEqual(actual, expected);

テストケースごとにこの分岐を書くのは嫌だ。

次に actual が 2 回出てくる。 1 回目は保存するものを指定するため 2 回目は比較するものを指定するため。これは必ず同じものになる。 1 回にしたい。 'name' も 2 回出てきてしまった。 snapshot をバラすのは難しそうだ。

それに Jest のような賢い matcher を備える可能性を考えるともとの object に戻して完全一致を狙うのは都合が悪い。そう考えると actual と同じ型の expected を返すつくりも良くない。

そこで assert を取り込んでしまう。

const assertMatchSnapshot = init(...); // assert などを設定

await assertMatchSnapshot('name', actual);

おおむね良さそうだ。

Jest と比較すると 'name' が気になるけど。これを消すのは一長一短だと思う。フレームワークと一体になっている Jest はテストケースの名前を適用可能だけど beater-snapshot は単体なので難しい。ちょっとした魔術になりそうだ。素直に 'name' を残して保存されている名前とすれば良さそうだ。

あと await だ。 assertError を投げることで assert の失敗を通知する。 Promise を返すつくりはそれらの挙動とあっていない。行儀は良くないけど fs の Sync 系の関数を使って同期的にする。

まとめると 0.2.0 の形になる。

import { Snapshot, init } from 'beater-snapshot';

// init の引数は既定値を例示のために挙げているだけで省略可能
const assertMatchSnapshot: (name: string, actual: any) => void = init({
  // snapshot を比較し不一致の場合に Error を投げる
  assert: (expected: string, actual: string): void =>
    assert.deepStrictEqual(JSON.parse(expected), JSON.parse(actual)),

  // snapshot の保存先ディレクトリ
  directory: path.resolve('__snapshots__'),

  // snapshot の作成
  stringify: (o: any): string =>
    JSON.stringify(o, null, 2),

  // save するか否かの判定
  update: process.env.UPDATE_SNAPSHOT === 'true'
});

assertMatchSnapshot('name', actual);

おしまい。

書いていて気づいたけど expected と actual の引数の順序がおかしい。 0.3.0 で直す。


Android アプリ設計パターン入門』を読んだ。他のアプリを見るのが良いのだろうな。そんな印象。教科書的な雰囲気かと思ったけど実例集的な雰囲気だった。


歯医者に行った (2019-11-18) 。なめらかになった。落ち着いて 2020 を迎えられそうだ。