ファイルシステムに依存するコードの単体テスト


138

私は、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またはネイティブコードにも適用できます。


8
ユニットテストではファイルシステムに書き込みを行わないでください。ファイルシステムに書き込もうとすると、ユニットテストの構成が理解できないからです。ユニットテストは通常​​、単一の実際のオブジェクト(テスト対象のユニット)と相互作用し、他のすべての依存関係がモックされて渡されます。テストクラスは、オブジェクトのメソッドを介した論理パスと、テスト中のユニット。
クリストファーペリー

1
あなたの状況では、ユニットテストが必要な唯一の部分は myDll.InvokeSomeSpecialMethod();は、成功と失敗の両方の状況で正しく機能することを確認、行いませんDoItDllRunner.Run、UNITテストを誤用してプロセス全体が機能することを再確認することです許容できる誤用、および単体テストを装った統合テストとなるため、通常の単体テストルールを厳密に適用する必要はありません
MikeT

回答:


47

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

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


27
ただし、+ 1 chdir()はプロセス全体に適用されるため、テストフレームワークまたはその将来のバージョンでサポートされている場合は、テストを並行して実行する機能を無効にする可能性があります。

69

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

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


12
前述のテスト可能なDoIt関数は、テストすら必要ありません。質問者が正しく指摘したように、テストすべき重要性は何も残っていません。今ではの実装だIZipperIFileSystemと、IDllRunnerそれはテストの必要が、彼らはテストのために嘲笑されている非常にものです!
Ian Goldby 14

56

あなたの質問は、開発者がそれに入るだけのテストの最も難しい部分の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あなたのメソッドの名前である(あるいは少なくともそれはする必要があります)。どのようにすべてのあなたの方法の内側に住む小さな実装の詳細です。優れたテストでは、WHATを壊すことなくHOWを交換できます。

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

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

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

ファイルシステムの依存関係を使用したコードのテストに関する特定の質問に答えるために、ファイルで少し興味深いことがあり、Base64でエンコードされたコンテンツをbyte[]ファイルに保存したいとします。これにストリームを使用して、コードがどのように動作するを確認する必要なく、コードが正しいことをテストできます。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");
}

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

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


1
ここでのメッセージに同意するかどうかはわかりません。この種のメソッドを単体テストする必要はないと言っていますか?TDDが悪いと言っているのですか?TDDを行う場合と同様に、最初にテストを記述せずにこのメソッドを記述することはできません。または、あなたの方法はテストを必要としないという直感に頼っていますか?すべての単体テストフレームワークに「検証」機能が含まれている理由は、それを使用しても問題ないためです。「メソッドの実装の詳細を変更するたびにテストを変更する必要があるため、これは悪いことです...」ユニットテストの世界へようこそ。
ロニー

2
メソッドのCONTRACTは、実装ではなくテストする必要があります。その契約の実装が変更されるたびにテストを変更する必要がある場合、アプリのコードベースとテストのコードベースの両方を維持するのは恐ろしい時間です。
クリストファーペリー

@Ronnieが盲目的に単体テストを適用することは役に立ちません。さまざまな性質のプロジェクトがあり、単体テストはそれらのすべてで効果的ではありません。例として、私は、コードの95%はであるプロジェクトに取り組んでいますに関する副作用(ノート、この副作用-重い性質があるという要件によって、それはだ、基本的な複雑さ、偶発的ではない、それはからデータを収集しているため、さまざまなステートフルソースがあり、操作がほとんどないため、純粋なロジックはほとんどありません)。単体テストはここで効果的ではなく、統合テストは効果的です。
Vicky Chijwani 2017

副作用はシステムの端に押しやるべきであり、レイヤー全体で絡み合ってはいけません。端では、動作である副作用をテストします。他のあらゆる場所で、副作用のない純粋な関数を使用するようにしてください。副作用は簡単にテストされ、簡単に推論、再利用、および構成できます。
クリストファーペリー

24

私は、ユニットテストを容易にするためだけに存在するタイプと概念でコードを汚染するのに抵抗しています。確かに、それによってデザインがよりクリーンで優れたものになれば、それはよくないことだと思います。

これについての私の見解は、あなたの単体テストはできる限り多くのことをするでしょう、それは100%のカバレッジではないかもしれません。実際、それは10%に過ぎないかもしれません。ポイントは、ユニットテストは高速で、外部依存関係がないことです。「このパラメーターにnullを渡すと、このメソッドはArgumentNullExceptionをスローする」などのケースをテストする場合があります。

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

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


5
うん、聞こえます。この奇妙な世界では、分離したところに到達し、抽象オブジェクトに対するメソッドの呼び出しだけが残ります。ふわふわふわふわ。この時点に到達すると、実際に何かを実際にテストしているようには感じられません。クラス間の相互作用をテストしているだけです。
ユダガブリエルヒマンゴ

6
この答えは見当違いです。ユニットテストはフロスティングのようなものではなく、砂糖のようなものです。ケーキに焼きました。それはあなたのコードを書くことの一部です...設計活動。したがって、テストはコードの記述を容易にするものであるため、「テストを容易にする」ものでコードを「汚染」することは決してありません。テストの前に開発者がコードを記述し、テスト不能な悪質なコード
Christopher Perry

1
@Christopher:アナロジーを拡張するために、砂糖を使用できるようにするために、ケーキがバニラのスライスに似たものになってほしくない。私が主張しているのは、実用主義だけです。
ケントブーガート2013年

1
@クリストファー:あなたの経歴はそれをすべて言います:「私はTDD熱狂者です」。一方、私は実用的です。私はTDDをそれが適合する場所ではなく行う-私の答えにはTDDを行わないことを示唆するものは何もないが、あなたはそれを考えているようだ。そしてそれがTDDであるかどうかにかかわらず、テストを容易にするために大量の複雑さを紹介することはしません。
Kent Boogaart 2013年

3
@ChristopherPerry OPの元の問題をTDDで解決する方法を説明できますか?私はいつもこれに遭遇します。この質問のように、外部依存関係のあるアクションを実行することのみを目的とする関数を記述する必要があります。では、テストを最初に書くシナリオでさえ、そのテストはどうなるでしょうか?
Dax Fohl 2014

8

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

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

私は同意します。ときどき、相互作用ベースのテストはカップリングが多すぎて、十分な価値が得られないことが多いと思います。ここでファイルの解凍をテストしたいのは、正しいメソッドを呼び出していることを確認するだけではありません。


どのくらいの頻度で実行するかはほとんど問題になりません。自動的に実行される継続的インテグレーションサーバーを使用します。私たちは彼らがどれくらいかかるかを本当に気にしません。「実行時間」が問題にならない場合、単体テストと統合テストを区別する理由はありますか?
ユダガブリエルヒマンゴ

4
あんまり。しかし、開発者がすべての単体テストをローカルですばやく実行したい場合は、それを簡単に行う方法があると便利です。
JC。

6

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


OK、それでストリームの注入が可能になります。依存性注入/ IOC。ストリームをファイルに解凍し、それらのファイル間でdllをロードし、そのdllのメソッドを呼び出す部分についてはどうでしょうか。
ユダガブリエルヒマンゴ

3

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

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


2

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


あなたとnsayerは本質的に同じ提案を持っています:私のコードをストリームで動作させることです。ストリームの内容をdllファイルに解凍し、そのdllを開いて関数を呼び出す部分についてはどうですか?そこで何をしますか?
ユダガブリエルヒマンゴ

3
@JudahHimango。これらのパーツは必ずしもテスト可能であるとは限りません。すべてをテストすることはできません。テスト不可能なコンポーネントを独自の機能ブロックに抽象化し、それらが機能すると想定します。このブロックの動作に関するバグに遭遇したら、テストを考案し、出来上がりです。単体テストは、すべてをテストする必要があるという意味ではありません。一部のシナリオでは、100%のコードカバレッジは非現実的です。
Zoran Pavlovic 2012年

1

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


1

ユニットテストの場合、プロジェクトにテストファイル(EARファイルまたは同等のもの)を含め、ユニットテストで相対パスを使用することをお勧めします(「../testdata/testfile」など)。

プロジェクトが正しくエクスポート/インポートされている限り、単体テストは機能するはずです。


0

他の人が言ったように、最初は統合テストとしては問題ありません。2つ目は、関数が実際に実行することになっていることだけをテストします。これはすべてユニットテストで実行する必要があります。

示されているように、2番目の例は少し無意味に見えますが、関数がどのステップのエラーに応答するかをテストする機会を与えてくれます。この例ではエラーチェックはありませんが、実際のシステムではそうである可能性があり、依存関係の注入により、エラーに対するすべての応答をテストできます。その後、コストはそれだけの価値があります。

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