他の純関数に委譲する共用体型で純関数をテストする


8

共用体型を取り、その型を絞り込み、他の2つの純粋関数のいずれかに委任する関数があるとします。

function foo(arg: string|number) {
    if (typeof arg === 'string') {
        return fnForString(arg)
    } else {
        return fnForNumber(arg)
    }
}

fnForString()およびfnForNumber()も純粋な関数であり、それらはすでにテスト済みであると想定します。

どのようにテストを行うべきfoo()ですか?

  • あなたはそれに委譲しているという事実扱うべきであるfnForString()fnForNumber()のためのテストを書くときに、本質的に、実装の詳細など、及びそれらのそれぞれのテストを複製するのfoo()?この繰り返しは受け入れられますか?
  • あなたは、「知っている」のテスト書くべきfoo()にデリゲートをfnForString()し、fnForNumber()それらをからかっし、それらへの委譲ことをチェックすることにより、例えば?

2
ハードコードされた依存関係を持つ独自のオーバーロードされた関数を作成しました。この種のポリモーフィズム(正確にはアドホックなポリモーフィズム)を実現する別の方法は、関数の依存関係を引数(型ディレクトリ)として渡すことです。次に、テスト目的でモック関数を使用できます。
ボブ2019年

わかりました、しかしそれは「モックを達成する方法」のより多くのケースです-それで、はい、あなたは関数を渡したり、カリー化された関数を持っているかもしれません。しかし、私の質問は「モックする方法」のレベルに関するものでした。しかし、むしろ「純粋な関数のコンテキストで正しいアプローチをあざけるのですか?」
samfrances

回答:


1

理想的な世界では、テストの代わりに証明を書くでしょう。たとえば、次の関数を考えます。

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回適用冪等、すべての有効な入力のために、すなわちxtransform(transform(x))に等しいですx。さて、あなたは最初にすることを証明する必要があるだろうnegatereverse2回適用冪等です。さて、の冪等性を証明したとするnegatereverse、コンパイラはそれを把握することができますつまり、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);
  }
};

ここでは多くのことが起こっているので、分解してみましょう。

  1. a|bユニオンタイプやa&b交差タイプと同じように、a≡bは等価タイプです。
  2. x等価タイプの値は、およびa≡bの等価の証明です。ab
  3. 2つの値abが等しくない場合、タイプの値を作成することはできませんa≡b
  4. 値はrefl反射性の略で、タイプがありa≡aます。それは、値がそれ自体と等しいことの些細な証明です。
  5. 私たちは、使用reflの証拠にnegateNegateIdempotentreverseReverseIdempotent。命題はコンパイラが自動的に証明するのに十分なほど簡単なので、これは可能です。
  6. 証明にはnegateNegateIdempotentreverseReverseIdempotent補題を使用し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()ですか?

関数型プログラミングは、プロパティベースのテストを促進します。例えば、私がテストしたnegatereversetransformの機能は冪等のために2回適用しました。プロパティベースのテストに従う場合、命題関数は、テストする関数と構造が類似している必要があります。

あなたはそれに委譲しているという事実扱うべきであるfnForString()fnForNumber()のためのテストを書くときに、本質的に、実装の詳細など、及びそれらのそれぞれのテストを複製するのfoo()?この繰り返しは受け入れられますか?

はい、受け入れられます。あなたは完全に放棄テストすることができ、もののfnForStringおよびfnForNumberそれらのためのテストはのためのテストに含まれているためfoo。ただし、完全を期すために、冗長性が導入されている場合でも、すべてのテストを含めることをお勧めします。

あなたは、「知っている」のテスト書くべきfoo()にデリゲートをfnForString()し、fnForNumber()それらをからかっし、それらへの委譲ことをチェックすることにより、例えば?

プロパティベースのテストで作成する命題は、テストする関数の構造に従います。したがって、テストされている他の関数の命題を使用して、依存関係について「知っています」。それらをあざける必要はありません。ネットワークコール、ファイルシステムコールなどのモックを作成するだけで済みます。


5

最善の解決策は、単にをテストすることfooです。

fnForStringおよびfnForNumberは、必ずしもの動作を変更せずに将来変更する可能性がある実装の詳細ですfoo。それが発生すると、テストが理由もなく中断する可能性があります。この種の問題は、テストを拡張しすぎて役に立たなくなります。

あなたのインターフェースはを必要としfoo、それをテストするだけです。

あなたはをテストする必要がある場合fnForStringfnForNumberパブリック・インタフェースのテストとは別にテストのこの種を維持します。

これはケントベックが述べた次の原則の私の解釈です

プログラマーテストは、動作の変化に敏感で、構造の変化に鈍感でなければなりません。プログラムの動作がオブザーバーの観点から安定している場合、テストは変更されません。


2

短い答え:関数の仕様は、それをテストする方法を決定します。

長い答え:

テスト=実装がその仕様を満たすことを確認するために、テストケースのセット(できれば、発生する可能性のあるすべてのケースを代表するもの)を使用します。

この例では、fooは仕様なしで記述されているため、まったく何もしないでfooをテストする必要があります(または「fooが何らかの方法で終了する」という暗黙の要件を検証するためのせいぜい愚かなテスト)。

仕様が「この関数は、argsのタイプに応じてfnForStringまたはfnForNumberのいずれかにargsを適用した結果を返す」などの操作可能な場合は、デリゲートをモックする(オプション2)の方法です。fnForString / Numberに何が起こっても、fooはその仕様に従っています。

仕様がこのような方法でfnForTypeに依存しない場合は、fnFortype(オプション1)のテストを再利用する方法があります(これらのテストが適切であると想定)。

運用仕様により、ある実装を別の実装(よりエレガントで読みやすく、効率的など)に置き換えるという通常の自由の多くが取り除かれていることに注意してください。慎重に検討してから使用してください。


1

fnForString()とfnForNumber()も純粋な関数であり、それら自体はすでにテスト済みであると想定します。

まあ実装の詳細をに委任されているためfnForString()fnForNumber()のためにstringnumber、それは単にそれを確認するために、つまるところテスト、それぞれfoo右の関数を呼び出します。だから、はい、私はそれらをあざけって、それに応じて呼び出されることを確認します。

foo("a string")
fnForNumberMock.hasNotBeenCalled()
fnForStringMock.hasBeenCalled()

個別にテストされているためfnForString()fnForNumber()foo()呼び出すと、適切な関数が呼び出され、その関数が想定どおりに機能することがわかります。

fooは何かを返す必要があります。それぞれ異なるモックから何かを返すことができ、fooが正しく返されることを確認できます(たとえば、foo関数returnでを忘れた場合)。

そして、すべてがカバーされています。


0

関数のタイプをテストするのは役に立たないと思います。システムはこれを単独で行うことができ、興味のあるオブジェクトの各タイプに同じ名前を付けることができます

サンプルコード

  //  fnForStringorNumber String Wrapper  
String.prototype.fnForStringorNumber = function() {
  return  this.repeat(3)
}
  //  fnForStringorNumber Number Wrapper  
Number.prototype.fnForStringorNumber = function() {
  return this *3
}

function foo( arg ) {
  return arg.fnForStringorNumber(4321)
}

console.log ( foo(1234) )       // 3702
console.log ( foo('abcd_') )   // abcd_abcd_abcd_

// or simply:
console.log ( (12).fnForStringorNumber() )     // 36
console.log ( 'xyz_'.fnForStringorNumber() )   // xyz_xyz_xyz_

私はおそらくコーディング技法の優れた理論家ではありませんが、多くのコード保守を行いました。コーディングの方法の有効性は具体的なケースでのみ判断できると思います。推測には証明の価値はありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.