IEA - ドキュメント変換のためのファイルベースのテスト戦略
by Douglas Campbell, Principal Engineer
I/E/A - 入力/予想/実績
以下の一連の機能の実装を提供していただけると幸いです。
// defines how to go from file to an instance of IN
abstract IN loadInput(File input);
// defines how to go from file to OUT instances
// throw FNF whenever there’s no expected file.
abstract OUT loadOutput(File expected) throws FileNotFoundException;
// invoked upon missing expected file or if actual != expected
abstract void storeResult(OUT result, File resultFile);
ドキュメント変換のテストの手間を省き、堅牢なテストセットを維持するために、多くのことができます。
つまり、関数ではなくファイルとしてテストケースを書くことが容易になります。
私は、テストコードに手を加えるよりも、ドキュメントの変換をテストするこの方法を非常に気に入っています。 まず、背景を説明します。
背景
私が長年にわたって書いてきたコードの多くは、あるフォーマットのドキュメントを別のフォーマットに変更することを核としています。ドキュメントとは、json、xml、avro、tab区切り、yamlなど、あらゆる種類のものを指していますが、あらゆるものをあらゆるものに、そしてまた元に戻します。
あるフィールドを別のフィールドにマッピングし、それを大文字にし、外部のデータソースから読み取った別の値と融合させ、さらに別の表現に出力する、というような些細なことを書くことの恐ろしさに、多くの開発者が目を丸くしています。
その新しい表現は、最初のドキュメントとはほとんど似ていないかもしれない。それが獣の性(さが)です。
要するに、データ変換を行うための要件は完全に恣意的なのです。
これが、ある開発者がこの種のものを嫌う理由だと思います。近道をして、テストコードに以下のようなものを放り込んでしまうのです...。
//CHECKSTYLE:OFF
private static final String CLICK = "a horrendously ugly line of input from an access log";
//CHECKSTYLE:ON
そして、いざ「印象」をテストするとなると、どうなるか。入力を文字列変数としてインラインでテストするのです。checkstyleを抑制したことで、すぐにスパイディセンスが働きました。
私が学んだことの一つは、恐怖や回避があるところには、それこそ過剰にエネルギーを使った怠惰が必要だということです。 一歩下がる。徹底的にシンプルにして、何度も何度も重複するロジックやコードを取り除くのです。テストの詰まったフォルダ全体を実行する関数を書きます。
// test folder of tests and fail entire run if any fail.
public void testFolder(File testfolder, Function<IN, OUT> converter) {
for (File test: testfolder.listFiles(testFileFilter)) {
if (!testSingle(test, converter)) {
fail("expected != actual - actual " +
"results saved in .actual file");
};
}
}
では、このtestSingle関数はどうでしょうか?ブログの冒頭で定義した3つの関数を使えば、とても簡単です。
// test a single test
private boolean testSingle(File test, Function converter) {
IN input = loadInput(test);
OUT actual = converter.apply(input);
OUT expected = null;
// only create if no expected file or result is different.
File actualFile = new File(test.getParent(),
test.getName() + ".actual");
try {
expected = loadOutput(new File(test.getParent(),
test.getName()
+ ".expected"));
} catch (FileNotFoundException ex) {
// we haven't got expected file - no biggie
// we can turn this into an expected file once
// satisfied with it.
storeResult(actual, actualFile);
}
return false;
// we've got something to compare
if (!actual.equals(expected)) {
// fail and save the actual file for command
// line diffing
storeResult(actual, actualFile);
return false;
}
return true;
}
このアプローチのその他の利点
テキストやアスキーベースのドキュメントでは、テストの失敗を比較するためのdiffユーティリティがすぐに利用できます。
期待通りの出力を生成するためにテストコードを書く必要はありません。サンプルレコードをtestsフォルダに追加してテストを実行するだけで、実際の出力が.actualという拡張子の新しいファイルに生成されます。
本番で問題となったレコードに対して、すぐにコードを実行することができます。もう一度、問題のあるレコードをテストフォルダにドロップし、テストを実行します。
あなたのチームが全く新しいコンバータを構築したり、異なるjsonライブラリを使用することを決定した場合、すべてのテストは言語に依存しないファイルとして表現されます。 それらを引き継ぐことができます。
正面から見たときのネタ
まず、変換ロジックをステートレスにすることに注意してください。 基本的に、プロセス外の呼び出しを含むドキュメント変換は、生データの取得とそれに対する処理を分離するために再構築する必要があります。
2つ目は、コードモジュールやアプリのライフサイクルの早い段階でこれを始めることです。 古いテストに戻ってやり直すことは、たいていの場合、あまり意味がありません。
次のステップ
次のステップとして、これをオープンソースのテストパッケージとして提供したいと考えています。少しでも興味を持っていただければ、もっと早く実現するかもしれません。