理想的な世界では、テストの代わりに証明を書くでしょう。たとえば、次の関数を考えます。
const negate = (x: number): number => -x;
const reverse = (x: string): string => x.split("").reverse().join("");
const transform = (x: number|string): number|string => {
switch (typeof x) {
case "number": return negate(x);
case "string": return reverse(x);
}
};
あなたはそれを証明したいと言っtransformている2回適用冪等、すべての有効な入力のために、すなわちx、transform(transform(x))に等しいですx。さて、あなたは最初にすることを証明する必要があるだろうnegateとreverse2回適用冪等です。さて、の冪等性を証明したとするnegateとreverse、コンパイラはそれを把握することができますつまり、2回適用することは自明です。したがって、次の補題があります。
const negateNegateIdempotent = (x: number): negate(negate(x))≡x => refl;
const reverseReverseIdempotent = (x: string): reverse(reverse(x))≡x => refl;
これら2つの補題を使用してtransform、次のようにべき等であることを証明できます。
const transformTransformIdempotent = (x: number|string): transform(transform(x))≡x => {
switch (typeof x) {
case "number": return negateNegateIdempotent(x);
case "string": return reverseReverseIdempotent(x);
}
};
ここでは多くのことが起こっているので、分解してみましょう。
a|bユニオンタイプやa&b交差タイプと同じように、a≡bは等価タイプです。
x等価タイプの値は、およびa≡bの等価の証明です。ab
- 2つの値
aとbが等しくない場合、タイプの値を作成することはできませんa≡b。
- 値は
refl、反射性の略で、タイプがありa≡aます。それは、値がそれ自体と等しいことの些細な証明です。
- 私たちは、使用
reflの証拠にnegateNegateIdempotentとreverseReverseIdempotent。命題はコンパイラが自動的に証明するのに十分なほど簡単なので、これは可能です。
- 証明には
negateNegateIdempotentとreverseReverseIdempotent補題を使用しtransformTransformIdempotentます。これは重要な証明の例です。
プルーフを作成する利点は、コンパイラがプルーフを検証することです。証明が正しくない場合、プログラムは型チェックに失敗し、コンパイラーはエラーをスローします。証明は2つの理由でテストよりも優れています。まず、テストデータを作成する必要はありません。すべてのエッジケースを処理するテストデータを作成することは困難です。次に、誤ってエッジケースをテストすることを忘れないようにします。コンパイラーがエラーをスローします。
残念ながら、TypeScriptは依存型、つまり値に依存する型をサポートしていないため、等価型はありません。したがって、TypeScriptで証明を書くことはできません。Agdaのような依存型付けの関数型プログラミング言語で証明を書くことができます。
ただし、TypeScriptで命題を記述できます。
const negateNegateIdempotent = (x: number): boolean => negate(negate(x)) === x;
const reverseReverseIdempotent = (x: string): boolean => reverse(reverse(x)) === x;
const transformTransformIdempotent = (x: number|string): boolean => {
switch (typeof x) {
case "number": return negateNegateIdempotent(x);
case "string": return reverseReverseIdempotent(x);
}
};
次に、jsverifyなどのライブラリを使用して、複数のテストケースのテストデータを自動的に生成できます。
const jsc = require("jsverify");
jsc.assert(jsc.forall("number", transformTransformIdempotent)); // OK, passed 100 tests
jsc.assert(jsc.forall("string", transformTransformIdempotent)); // OK, passed 100 tests
で電話jsc.forallをかけることもできますが"number | string"、うまく機能しないようです。
だからあなたの質問に答えるために。
どのようにテストを行うべきfoo()ですか?
関数型プログラミングは、プロパティベースのテストを促進します。例えば、私がテストしたnegate、reverseとtransformの機能は冪等のために2回適用しました。プロパティベースのテストに従う場合、命題関数は、テストする関数と構造が類似している必要があります。
あなたはそれに委譲しているという事実扱うべきであるfnForString()とfnForNumber()のためのテストを書くときに、本質的に、実装の詳細など、及びそれらのそれぞれのテストを複製するのfoo()?この繰り返しは受け入れられますか?
はい、受け入れられます。あなたは完全に放棄テストすることができ、もののfnForStringおよびfnForNumberそれらのためのテストはのためのテストに含まれているためfoo。ただし、完全を期すために、冗長性が導入されている場合でも、すべてのテストを含めることをお勧めします。
あなたは、「知っている」のテスト書くべきfoo()にデリゲートをfnForString()し、fnForNumber()それらをからかっし、それらへの委譲ことをチェックすることにより、例えば?
プロパティベースのテストで作成する命題は、テストする関数の構造に従います。したがって、テストされている他の関数の命題を使用して、依存関係について「知っています」。それらをあざける必要はありません。ネットワークコール、ファイルシステムコールなどのモックを作成するだけで済みます。