150

ZIP ファイルが与えられた場合、次のことを行う必要があるコンポーネントを作成しています。

  1. ファイルを解凍します。
  2. 解凍されたファイルの中から特定の dll を見つけます。
  3. リフレクションを介してその dll をロードし、メソッドを呼び出します。

このコンポーネントを単体テストしたいと思います。

ファイルシステムを直接扱うコードを書きたくなります:

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

しかし、「ファイル システム、データベース、ネットワークなどに依存する単体テストを作成しないでください」とよく言われます。

これを単体テストに適した方法で記述するとしたら、次のようになると思います。

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

わーい!これでテスト可能になりました。テスト ダブル (モック) を DoIt メソッドにフィードできます。しかし、どのくらいの費用がかかりますか?これをテスト可能にするためだけに、3 つの新しいインターフェースを定義する必要がありました。そして、正確には、何をテストしていますか?DoIt 関数がその依存関係と適切に相互作用することをテストしています。zip ファイルが正しく解凍されたかどうかなどのテストは行いません。

もう機能をテストしているようには感じません。クラスの相互作用をテストしているだけのように感じます。

私の質問はこれです:ファイルシステムに依存するものを単体テストする適切な方法は何ですか?

編集私は.NETを使用していますが、この概念はJavaまたはネイティブコードにも適用できます。

4

11 に答える 11

73

わーい!今ではテスト可能です。テストダブル(モック)をDoItメソッドにフィードできます。しかし、どのくらいの費用がかかりますか?これをテスト可能にするためだけに、3つの新しいインターフェイスを定義する必要がありました。そして、正確には、私は何をテストしていますか?DoIt関数がその依存関係と適切に相互作用することをテストしています。zipファイルが正しく解凍されたかどうかなどはテストされません。

あなたはその頭に釘を打ちました。テストしたいのはメソッドのロジックであり、必ずしも実際のファイルをアドレス指定できるかどうかではありません。(この単体テストでは)ファイルが正しく解凍されているかどうかをテストする必要はありません。メソッドはそれを当然のことと見なします。インターフェイスは、1つの具体的な実装に暗黙的または明示的に依存するのではなく、プログラミングできる抽象化を提供するため、それ自体が価値があります。

于 2010-11-02T07:15:03.490 に答える
67

あなたの質問は、開発者にとってテストの最も難しい部分の1つを明らかにしています。

「一体何をテストするのですか?」

あなたの例は、いくつかの API 呼び出しを結合するだけなので、あまり興味深いものではありません。そのため、単体テストを作成すると、メソッドが呼び出されたことをアサートすることになります。このようなテストは、実装の詳細をテストに密接に結合します。実装の詳細を変更するとテストが壊れるため、メソッドの実装の詳細を変更するたびにテストを変更する必要があるため、これは悪いことです。

悪いテストを行うことは、テストをまったく行わないことよりも実際には悪いことです。

あなたの例では:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

モックを渡すことはできますが、テストするメソッドにはロジックがありません。これについて単体テストを試みるとしたら、次のようになります。

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

おめでとうございます。基本的に、メソッドの実装の詳細をコピーDoIt()してテストに貼り付けました。ハッピーメンテ。

テストを作成するときは、 HOWではなくWHATをテストする必要があります。 詳細については、ブラック ボックス テストを参照してください。

WHATはメソッドの名前です (または、少なくともそうあるべきです)。HOWは、メソッド内に存在するすべての小さな実装の詳細です。優れたテストでは、 WHATを壊さずにHOWを入れ替えることができます。

このように考えて、次のように自問してください。

「このメソッドの実装の詳細を (パブリック コントラクトを変更せずに) 変更すると、テストが中断されますか?」

答えが「はい」の場合、 WHATではなくHOWをテストしています。

ファイル システムの依存関係を使用したコードのテストに関する特定の質問に答えるために、ファイルでもう少し興味深いことがあり、Base64 でエンコードさbyte[]れた a のコンテンツをファイルに保存したいとします。これにストリームを使用して、コードがどのように動作するかを確認することなく、コードが正しい動作をするかどうかをテストできます。1つの例は、次のようなものです(Javaで):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

テストでは を使用しますByteArrayOutputStreamが、アプリケーション (依存性注入を使用) では、実際の StreamFactory (おそらく FileStreamFactory と呼ばれる) がFileOutputStreamから戻り、outputStream()に書き込みFileます。

このメソッドで興味深いのは、writeBase64 でエンコードされたコンテンツを書き込んでいたことです。これをテストしたのです。あなたの方法では、これは統合テストDoIt()でより適切にテストされます。

于 2014-04-17T04:52:55.300 に答える
54

これには何も問題はありません。これを単体テストと呼ぶか統合テストと呼ぶかの問題です。ファイルシステムと対話する場合、意図しない副作用がないことを確認する必要があります. 具体的には、作成した一時ファイルをすべて削除してからクリーンアップし、使用していた一時ファイルと同じファイル名を持つ既存のファイルを誤って上書きしないようにしてください。絶対パスではなく、常に相対パスを使用してください。

chdir()テストを実行する前に一時ディレクトリに移動し、chdir()後で戻ることもお勧めします。

于 2008-09-24T19:09:31.710 に答える
25

単体テストを容易にするためだけに存在する型や概念でコードを汚染することには消極的です。確かに、それがデザインをよりクリーンで優れたものにするのであれば、それは素晴らしいことではありませんが、多くの場合、そうではないと思います.

これについての私の見解は、単体テストは 100% のカバレッジではない可能性があるため、できる限り多くのことを行うということです。実際、それはわずか10%かもしれません。要点は、単体テストは高速で、外部依存関係がないようにする必要があるということです。「このパラメーターに null を渡すと、このメソッドは ArgumentNullException をスローする」などのケースをテストする場合があります。

次に、外部依存関係を持つことができる統合テスト (これも自動化され、おそらく同じ単体テスト フレームワークを使用) を追加し、これらのようなエンド ツー エンドのシナリオをテストします。

コード カバレッジを測定するときは、単体テストと統合テストの両方を測定します。

于 2008-09-24T18:51:59.833 に答える
8

ファイルシステムにアクセスしても問題はありません。単体テストではなく統合テストと考えてください。ハードコードされたパスを相対パスと交換し、単体テストのzipを含むTestDataサブフォルダーを作成します。

統合テストの実行に時間がかかりすぎる場合は、それらを分離して、クイック単体テストほど頻繁に実行されないようにします。

私は同意します。相互作用ベースのテストは結合度が高くなりすぎて、十分な価値を提供できなくなることがよくあると思います。適切なメソッドを呼び出していることを確認するだけでなく、ここでファイルの解凍をテストする必要があります。

于 2008-09-24T18:57:31.693 に答える
6

1 つの方法は、InputStreams を取得する unzip メソッドを記述することです。次に、単体テストは、ByteArrayInputStream を使用して、バイト配列からそのような InputStream を構築できます。そのバイト配列の内容は、単体テスト コードの定数である可能性があります。

于 2008-09-24T18:49:49.010 に答える
3

理論的には、変更される可能性のある特定の詳細(ファイルシステム)に依存しているため、これは統合テストのようです。

OSを処理するコードをそれ自体のモジュール(クラス、アセンブリ、jarなど)に抽象化します。あなたの場合、見つかった場合は特定のDLLをロードしたいので、IDllLoaderインターフェースとDllLoaderクラスを作成します。アプリは、インターフェイスを使用してDllLoaderからDLLを取得し、それをテストしますか?..結局、解凍コードの責任はありませんか?

于 2008-09-24T19:00:37.317 に答える
2

「ファイルシステムの相互作用」がフレームワーク自体で十分にテストされていると仮定して、ストリームを操作するメソッドを作成し、これをテストします。FileStream.Open はフレームワークの作成者によって十分にテストされているため、FileStream を開いてメソッドに渡すことはテストから除外できます。

于 2008-09-24T18:50:32.927 に答える
1

クラスの相互作用と関数の呼び出しをテストしないでください。代わりに、統合テストを検討する必要があります。ファイルの読み込み操作ではなく、必要な結果をテストします。

于 2008-09-24T18:52:05.573 に答える