読者です 読者をやめる 読者になる 読者になる

Make Your Tests Deterministicを翻訳してみた。

node.js

How to Node にしばらく前に投稿されたMake your test deterministicを翻訳していこうと思います。
ちなみに元記事はこちら。
Make Your Tests Deterministic - How To Node - NodeJS

テストする時、実行したら結果が変わるものって結果をどう評価するか困らないですか?

自分は困っていました。このテスト、次実行したらまた結果変わるんだけどなぁとか思ってました。
どうするんだろうと思っていたので参考になる所もあるかと思います。

本エントリでは、実行した時に非確実性があまりなく、結果がシミュレート可能なテスト = 確定的なテスト
実行した時に結果が不確実なテスト = 不確定的なテスト
と呼ぶことにします。

Make your tests deterministic 確定的なテストをしていこう。


競合状態やデッドロックのような不確定的な問題はテストするのが難しい、再現するのが難しいからだ。幸運なことに、javascriptの世界では、単一のスレッドしか持たない、だから我々は安全だ。。。そうだろうか?

いや、本当は違う。
callbackの実行順序がマルチスレッド環境でも同じく発生する「競合状態」を引き起こす可能性がある。

Example


ファイル集合の最終更新日時をタイムスタンプとして表し、配列にして返す関数があるとする。

var fs = require('fs');
var getLastModified = function(files, done) {
  var timestamps = [];
  files.forEach(function(file) {
    fs.stat(file, function(err, stat) {
      timestamps.push(stat.mtime);
      if (timestamps.length === files.length) {
        done(null, timestamps);
      }
    });
  });
};

このコードを単体テストするために、fs モジュールのモックを作るのが必要になる。(本当のファイルシステムを利用すると、初期状態の設定や、後で削除する必要があり、結果としてテストが遅くなる。)fs モジュールのモックはnode-mocksで簡単になる。node-mocksを使えば、インメモリファイルシステムのモックが利用できる。

// Jasmine Syntax
describe('getLastModified', function() {
  var mocks = require('mocks');
  var mockery = {
    // in-memoryのニセfsを作る
    fs: mocks.fs.create({
      'one.js': mocks.fs.file('2012-01-01'),
      'two.js': mocks.fs.file('2012-02-02'),
      'three.js': mocks.fs.file('2012-02-02')
    })
  };

  // ニセfsを使ってモジュールをロード
  var getLastModified = mocks.loadFile('get-last-modified.js', mockery).getLastModified;

  it('should return last modified timestamps for every file', function() {
    var spy = jasmine.createSpy('done').andCallFake(function(err, timestamps) {
      expect(timestamps).toEqual([
        new Date('2012-01-01'), new Date('2012-02-02'), new Date('2012-02-02')
        ]);
    });

    getLastModified(['/one.js', '/two.js', '/three.js'], spy);

    // コールバックの実行を待つ
    waitsFor(function() {return spy.callCount;});
  });
});

このユニットテストは成功する、しかし実際のコードの方はそうは行かない。更に悪いことに、毎回失敗するというわけではなく、時々成功して、時々失敗するという状況になる。。。この問題はこのコードが mock で作られた fs.stat コールバックの順序に依存してしまっており、本当の fs 上でこの順序を保証することは難しいからだ。このように不確定的になってしまう。

Solution


これに対して、テストを通じて複雑な振る舞いをシミュレートする必要がある。

ランダムの順序でコールバックを実行するニセのfsを作ることが出来れば、本当のfsの振る舞いとほとんど変わらない状況になる。しかしながら、それは時々成功して、時々失敗する不安定なテストの結果になる、先程のコードのように。

ストレステストとして、何回もテストを実行すれば、失敗の確率はより大きくなるだろう、でもそれではまだまだ不安定だ。

我々が本当に欲しい要件は常に確定的なテストだ。信頼性が高く、安定していることだ。だからこの振る舞いを予測可能かつコントロールされた方法でシミュレートする必要がある。fsモックがprocess.nextTickによく似た振る舞いをして、特定の順序でコールバックが呼ばれるpredictableNextTickを呼び出せれば良いのだ。

我々のユニットテストに以下の一行を追加しよう。

mocks.predictableNextTick.pattern = [1, 0];

突然だが、このテストは失敗している、なぜなら fs.stat のコールバックが登録したコールバックとは異なる順序で呼ばれるからだ。これは、second callbackが最初に呼ばれ、そのあと first callback, 次に fourth callback, さらに third callbackといったパターンで固定してコールバックが実行される。

重要なことは、この順序が毎回固定されて呼ばれるということだ。予測可能で確定的だ。

これは非同期的なAPIが扱われる時に発生する問題のとてもシンプルな解決策の例だ。同じタイプの問題はウェブアプリケーションでも発生する。例えば、多様なxhr リクエストを送る場合などだ。稼働中では、この振る舞いはコントロールから外れる。しかしながら、我々のコードの正しい振る舞いを保証するためには、全状況を正しくハンドリングする必要がある。ベストな解法は確定的な方法で全てがコントロールされた状況でシミュレートすることだ。

from scratch的感想


こんなのあったのね。node-mocks
fsのテストは難しいとは聞いていたけど、node-mocksはこの辺りの複雑なところをなんとかしてくれるみたい。また、in-memoryでそれっぽいfsを作ってくれる所は素晴らしい。使ってまたレポートしてみようかな。