Zipファイルを指定すると、次のことを行う必要があるコンポーネントを作成しています。
このコンポーネントを単体テストしたいと思います。
私はファイルシステムを直接扱うコードを書きたいと思っています:
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ファイルが適切に解凍されたかどうかはテストされません。
私はもう機能をテストしているようには感じません。クラスの相互作用をテストしているように感じます。
私の質問はこれです:ファイルシステムに依存する何かを単体テストする適切な方法は何ですか?
edit.NETを使用していますが、概念はJavaまたはネイティブコードも適用できます。
これには本当に何も問題はありません。それを単体テストと呼ぶか、統合テストと呼ぶかという問題です。ファイルシステムとやり取りする場合、意図しない副作用がないことを確認する必要があります。具体的には、自分でクリーンアップした(作成した一時ファイルを削除する)ことと、使用していた一時ファイルと同じファイル名を持つ偶然に既存のファイルを誤って上書きしないことを確認してください。絶対パスではなく、常に相対パスを使用してください。
また、テストを実行する前に一時ディレクトリにchdir()
し、その後chdir()
に戻るのも良い考えです。
わーい!今ではテスト可能です。 DoItメソッドにテストダブル(モック)をフィードできます。しかし、費用はいくらですか?これをテスト可能にするために、3つの新しいインターフェイスを定義する必要がありました。そして、正確に、私は何をテストしていますか? DoIt関数が依存関係と適切に相互作用することをテストしています。 Zipファイルが適切に解凍されたかどうかはテストされません。
頭に釘を打ちました。テストしたいのはメソッドのロジックであり、必ずしも真のファイルに対処できるかどうかではありません。ファイルが正しく解凍されているかどうかを(この単体テストで)テストする必要はありません。メソッドはそれを当然のことと見なします。インターフェースは、1つの具体的な実装に暗黙的または明示的に依存するのではなく、プログラム可能な抽象化を提供するため、それ自体で価値があります。
あなたの質問は、ちょうどそれに入る開発者のためのテストの最も難しい部分の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()
メソッドの実装の詳細をテストにコピーアンドペーストしました。幸せな維持。
テストを書くとき、[〜#〜] what [〜#〜]ではなくをテストしたい)[〜#〜] how [〜#〜]。詳細については、 ブラックボックステスト を参照してください。
[〜#〜] what [〜#〜]はメソッドの名前です(または、少なくともそうあるべきです)。 [〜#〜] how [〜#〜]はすべて、メソッド内に存在する小さな実装の詳細です。優れたテストでは、[〜#〜] how [〜#〜]を[ 〜#〜] what [〜#〜]。
このように考えて、自問してください:
"(パブリックコントラクトを変更せずに)このメソッドの実装の詳細を変更すると、テストが中断されますか?"
答えが「はい」の場合、[〜#〜] how [〜#〜]ではなくをテストしています。 )[〜#〜] what [〜#〜]。
ファイルシステムの依存関係を使用したコードのテストに関する特定の質問に答えるために、ファイルでもう少し面白いことがあり、_byte[]
_のBase64エンコードされたコンテンツをファイルに保存するとします。これにストリームを使用すると、コードが正しいことを実行することをテストできますhow。 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と呼ばれます)はoutputStream()
からFileOutputStream
を返し、File
。
ここでwrite
メソッドについて興味深いのは、Base64でエンコードされたコンテンツを書き込むことだったので、テストしました。 DoIt()
メソッドの場合、これは 統合テスト でより適切にテストされます。
ユニットテストを容易にするためだけに存在する型と概念でコードを汚染することは控えています。確かに、それがデザインをよりきれいに、そしてより良くするなら、それはしばしばそうではないと思います。
これに対する私の見解は、ユニットテストができる限りのことをするということであり、それは100%のカバレッジではないかもしれません。実際、10%に過ぎない場合があります。重要なのは、単体テストは高速で、外部の依存関係がないことです。 「このパラメーターにnullを渡すと、このメソッドはArgumentNullExceptionをスローする」などのケースをテストします。
次に、外部依存関係を持ち、これらのようなエンドツーエンドのシナリオをテストできる統合テスト(自動化され、おそらく同じユニットテストフレームワークを使用)を追加します。
コードカバレッジを測定するときは、単体テストと統合テストの両方を測定します。
ファイルシステムにアクセスしても問題はありません。単体テストではなく統合テストと考えてください。ハードコーディングされたパスを相対パスに交換し、単体テスト用のzipを含むTestDataサブフォルダーを作成します。
統合テストの実行に時間がかかりすぎる場合は、クイックユニットテストほど頻繁に実行されないように分離します。
私は同意します。時には、相互作用ベースのテストがカップリングを過度に引き起こし、多くの場合、十分な価値を提供しないと思うと思います。正しいメソッドを呼び出していることを確認するだけでなく、ここでファイルの解凍をテストする必要があります。
1つの方法は、InputStreamを取得するunzipメソッドを記述することです。次に、単体テストは、ByteArrayInputStreamを使用してバイト配列からこのようなInputStreamを構築できます。そのバイト配列の内容は、単体テストコードの定数である可能性があります。
理論的には変更される可能性のある特定の詳細(ファイルシステム)に依存しているため、これは統合テストのようです。
OSを扱うコードを独自のモジュール(クラス、アセンブリ、jarなど)に抽象化します。あなたの場合、特定のDLLが見つかったらロードするので、IDllLoaderインターフェイスとDllLoaderクラスを作成します。アプリでDllLoaderからDLLインターフェースとテスト..結局、解凍コードの責任はありませんか?
「ファイルシステムの相互作用」がフレームワーク自体で十分にテストされていると仮定して、ストリームを操作するメソッドを作成し、これをテストします。 FileStream.Openはフレームワークの作成者によって十分にテストされているため、FileStreamを開いてメソッドに渡すことはテストから除外できます。
クラスの相互作用と関数呼び出しをテストしないでください。代わりに、統合テストを検討する必要があります。ファイルの読み込み操作ではなく、必要な結果をテストします。
単体テストの場合は、プロジェクトにテストファイル(EARファイルまたは同等のもの)を含め、単体テストで相対パス、つまり「../testdata/testfile」を使用することをお勧めします。
プロジェクトが正しくエクスポート/インポートされている限り、単体テストは機能します。
他の人が言ったように、最初は統合テストとしては問題ありません。 2番目は、関数が実際に行うことになっていることのみをテストします。これは、すべて単体テストで行う必要があります。
示されているように、2番目の例は少し無意味に見えますが、関数がどのステップでエラーにどのように応答するかをテストする機会を与えてくれます。この例ではエラーチェックは行われていませんが、実際のシステムでは、依存関係の注入によってエラーに対するすべての応答をテストできます。そうすれば、コストはそれだけの価値があります。