静的メソッドを使用しているメソッドを単体テストするにはどうすればよいですか?


8

次のように、配列用の拡張メソッドをC#byte16進文字列にエンコードする拡張メソッドを記述したとします。

public static class Extensions
{
    public static string ToHex(this byte[] binary)
    {
        const string chars = "0123456789abcdef";
        var resultBuilder = new StringBuilder();
        foreach(var b in binary)
        {
            resultBuilder.Append(chars[(b >> 4) & 0xf]).Append(chars[b & 0xf]);
        }
        return resultBuilder.ToString();
    }
}

次のように、NUnitを使用して上記のメソッドをテストできます。

[Test]
public void TestToHex_Works()
{
    var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
    Assert.AreEqual("0123456789abcdef", bytes.ToHex());
}

Extensions.ToHexプロジェクト内を使用する場合はFoo.Do、次のようにメソッドを想定します。

public class Foo
{
    public bool Do(byte[] payload)
    {
        var data = "ES=" + payload.ToHex() + "ff";
        // ...
        return data.Length > 5;
    }
    // ...
}

次に、のすべてのテストはFoo.Doの成功に依存しTestToHex_Worksます。

C ++でフリー関数を使用しても結果は同じです。フリー関数を使用するメソッドをテストするテストは、フリー関数テストの成功に依存します。

どうすればこのような状況に対処できますか?これらのテストの依存関係をどうにかして解決できますか?上記のコードスニペットをテストするより良い方法はありますか?


4
Then all tests of Foo.Do will depend on the success of TestToHex_works - そう?他のクラスの成功に依存するクラスはありませんか?
Robert Harvey

5
私は、自由/静的関数へのこだわりと、いわゆる非テスト性を完全には理解していません。自由関数に副作用がない場合、それが機能することをテストして証明することは、地球上で最も簡単なことです。あなた自身の質問でこれを非常に効果的に示しました。オブジェクトのインスタンスで、通常の副作用のないメソッド(クラスの状態に依存しない)をどのようにテストしますか?私はあなたがそれらのいくつかを持っていることを知っています。
Robert Harvey

2
静的関数を使用するこのコードの唯一の欠点は、他の何かtoHex(またはスワップ実装)を簡単に使用できないことです。それを除いて、すべてがうまくいきます。16進数に変換するコードがテストされました。テストされたコードをユーティリティとして使用して、独自の目標を達成する別のコードがあります。
Steve Chamaillard

3
私はここで何が問題なのか完全に見逃しています。ToHexが機能しない場合、Doも機能しないことは明らかです。
Simon B

5
Foo.Do()のテストは、内部でToHex()を呼び出すことを知らないか、気にする必要がありません。これは実装の詳細です。
19 of 26

回答:


37

次に、のすべてのテストはFoo.DoTestToHex_Works の成功に依存します。

はい。そのため、のテストがありTextToHexます。これらのテストに合格すると、関数はそれらのテストで定義された仕様を満たします。だから、Foo.Do安全にそれを心配して電話してすることはできません。それはすでにカバーされています。

インターフェースを追加し、メソッドをインスタンスメソッドにして、に挿入できFooます。その後、あなたはモックすることができますTextToHex。しかし今、あなたはモックを書かなければなりません、それは異なって機能するかもしれません。したがって、2つを統合してパーツが実際に連携することを確認するには、「統合」テストが必要です。物事をより複雑にする以外に何が達成されましたか

単体テストではコードの一部を他の部分から分離してテストするべきだという考えは誤りです。単体テストの「ユニット」は、実行の分離されたユニットです。2つのテストを互いに影響を与えずに同時に実行できる場合、それらは分離して実行されるため、単体テストも実行されます。高速で、複雑な設定がなく、例のような副作用がない静的関数は、ユニットテストで直接使用しても問題ありません。コードが遅い、設定が複雑である、または副作用がある場合は、モックが役立ちます。ただし、他の場所では避けてください。


2
あなたは「テストファースト」開発についてかなり良い議論をします。モックが存在する理由の1つは、コードがテストするのが難しすぎるような方法で記述されているためです。最初にテストを記述した場合、テストしやすいコードを記述しなければなりません。
ロバートハーベイ

3
私は純粋な関数をモックしないことを推奨するために純粋に賛成しています。何度も見ました。
Jared Smith

2

C ++でフリー関数を使用しても結果は同じです。フリー関数を使用するメソッドをテストするテストは、フリー関数テストの成功に依存します。

どうすればこのような状況に対処できますか?これらのテストの依存関係をどうにかして解決できますか?上記のコードスニペットをテストするより良い方法はありますか?

まあ、ここには依存関係はありません。少なくとも、あるテストを別のテストの前に実行することを強制する種類ではありません。テスト間で構築する依存関係(種類に関係なく)は、1つの信頼です。

コードの一部(テストファーストかどうか)を作成し、テストに合格することを確認します。次に、より多くのコードを構築する立場にいます。最初にこれに基づいて構築されたすべてのコードは、信頼性と確実性に基づいて構築されています。これは多かれ少なかれ、@ DavidArnoが彼の回答で(非常によく)説明していることです。

はい。Xのテストがあるのはそのためです。これらのテストに合格すると、関数はそれらのテストで定義された仕様を満たします。したがって、Yはそれを安全に呼び出すことができ、心配する必要はありません。それはすでにカバーされています。

ユニットテストは、任意の順序、時間、環境で、可能な限り高速に実行する必要があります。TestToHex_Works最初と最後のどちらが実行されるかは問題ではありません。

TestToHex_Worksエラーが原因で失敗した場合、ToHex依存しているすべてのテストToHexは異なる結果で終了し、(理想的には)失敗します。ここで重要なのは、それらの異なる結果を検出することです。ユニットテストを確定的にすることでそれを行います。ここにいるように

var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
Assert.AreEqual("0123456789abcdef", bytes.ToHex());

さらに依存する単体テストToHexも決定論的でなければなりません。すべてToHexがうまくいけば、期待どおりの結果になるはずです。別のものを取得した場合、どこかで問題が発生しました。これは、これらの微妙な変更を検出して迅速に失敗するために、単体テストに必要なものです。


1

そのような最小限の例では少しトリッキーですが、私たちが何をしているのかをもう一度考えてみましょう:

次のように、16進文字列にエンコードするバイト配列の拡張メソッドをc#で記述したとします。

これを行う拡張メソッドを作成したのはなぜですか?バイト配列を文字列にエンコードするライブラリを作成しているのでない限り、これが満たそうとしている要件ではない可能性があります。ここで行っているように見えるのは、「バイト配列であるペイロードを検証する」という要件を満たそうとしていることです。つまり、実装しているユニットテストは、「有効なペイロードXを与えると、私のメソッドはtrueを返す」および「与えられる無効なペイロードY、私のメソッドはfalseを返します。

そのため、これを最初に実装するときに、メソッド内で「バイトから16進数」の要素をインラインで実行する場合があります。そして、それは結構です。その後、他の要件(「ペイロードを16進文字列として表示する」など)を取得しますが、それを実装しているときに、バイト配列を16進文字列に変換する必要があることもわかります。その時点で、拡張メソッドを作成し、古いメソッドをリファクタリングして、新しいコードから呼び出します。検証機能のテストは変更しないでください。ペイロードを表示するためのテストは、バイトから16進数へのコードがインラインでも静的メソッドでも同じである必要があります。静的メソッドは、テストを作成するときに気にする必要のない実装の詳細です。静的メソッドのコードは、そのコンシューマーによってテストされます。私は


「これは満たそうとしている要件ではない可能性があります。ここで行っているように見えるのは、要件を満たそうとしていることです」-おそらく、関数の目的について多くの仮定を行っています代表的な例として選ばれただけで、大規模なプログラムからのものではありません。
whatsisname

「単体テスト」の「単体」は、単一の関数またはクラスを意味するものではありません。それは機能の単位を意味します。バイトを文字列に変換するライブラリを具体的に記述しているのでない限り、その部分は、より有用な動作の実装の詳細です。その有用な動作は、テストを行いたい部分です。なぜなら、それが正しいことを気にするビットだからです。理想的な世界では、bin-to-hexを実行するためにすでに存在するライブラリー(または、それが何であれ、詳細にこだわらないでください)を見つけ、実装用に交換して、何も変更しないようにしたいテスト。
クリスクーパー

悪魔は細部にあります。ユニットテストの「実装の詳細」は、依然として非常に有用なものであり、ざっと読むものではありません。
whatsisname

0

丁度。これは静的メソッドの問題の1つです。もう1つは、ほとんどの状況でOOPがはるかに優れた代替手段であることです。

これは、依存性注入が使用される理由の1つでもあります。

あなたのケースでは、いくつかの値を16進数に変換する必要があるクラスに注入する具体的なコンバーターを持つことを好むかもしれません。インターフェースこの具象クラスで実装は、値を変換するために必要な契約を定義します。

コードを簡単にテストできるだけでなく、後で実装を入れ替えることもできます(値を16進数に変換する方法は他にもたくさんあり、いくつかは異なる出力を生成するため)。


2
コードがコンバーターテストの成功に依存しないようにするにはどうすればよいでしょうか。クラス、インターフェース、または必要なものを注入しても、魔法のように機能するわけではありません。副作用について話していれば、はい、実装をハードコーディングする代わりに、偽物を注入する大量の値がありますが、そもそもその問題は存在しませんでした。
Steve Chamaillard

1
@ArseniMourzenkoだから、multiplyメソッドを実装する計算機クラスを注入するとき、あなたはこれをあざけるでしょうか?つまり、リファクタリングする(そして*演算子を使用する)には、コード内のすべてのモック宣言をすべて変更する必要がありますか。私には簡単なコードのようには見えません。私が言っているのtoHexは非常に単純で副作用はまったくないので、これをモックまたはスタブする理由はありません。それはあまりにもコストがかかりすぎて、全く利益がありません。注入すべきではないと言っているわけではありませんが、それは使用方法によって異なります。
Steve Chamaillard

2
私は、コードの一部を変更し、「直面した場合@Ewanは、赤の海」、私は可能性がああ、いや、どこが起動しないと思います?。しかし、たぶん、変更したばかりのコードに戻って、最初にそのテストを見て、何が壊れているかを確認することもできます。:p
David Arno

3
@ ArseniMourzenko、DIによってデザインが大幅に改善される場合は、Stringやなどの型を注入するための引数が表示されないのはなぜintですか?または、標準ライブラリの基本的なユーティリティクラスですか?
Bart van Ingen Schenau

4
あなたの答えは実際的な側面を完全に無視します。高速で自己完結型であり、変更される可能性がほとんどない静的メソッドには多くの状況があり、それらを注入したり、通常の関数呼び出し以外のフープを飛び越えたりすることは、無意味な時間の無駄であり、努力。
whatsisname
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.