ここでStack Overflowの既存のユニットテスト関連のスレッドを読んで、ファイルI/O操作をユニットテストする方法について明確な答えのあるスレッドを見つけることができませんでした。私は最近、ユニットテストの検討を始めました。以前は利点を知っていましたが、最初にテストを書くことに慣れるのは困難でした。 NUnitとRhino Mocksを使用するようにプロジェクトを設定しましたが、その背後にある概念は理解していますが、Mockオブジェクトの使用方法を理解するのに少し苦労しています。
具体的には、2つの質問にお答えします。最初に、ユニットテストファイルI/O操作の適切な方法は何ですか?第二に、ユニットテストについて学ぼうとする試みで、依存性注入に遭遇しました。 Ninjectをセットアップして動作させた後、ユニットテスト内でDIを使用するのか、それともオブジェクトを直接インスタンス化するのかを考えていました。
チュートリアルからTDD を使用して、 Rhino Mocks および SystemWrapper を使用してください。
SystemWrapperは、File、FileInfo、Directory、DirectoryInfoなどを含むSystem.IOクラスの多くをラップします。 完全なリスト を確認できます。
このチュートリアルでは、MbUnitでテストを行う方法を示していますが、NUnitでもまったく同じです。
テストは次のようになります。
[Test]
public void When_try_to_create_directory_that_already_exists_return_false()
{
var directoryInfoStub = MockRepository.GenerateStub<IDirectoryInfoWrap>();
directoryInfoStub.Stub(x => x.Exists).Return(true);
Assert.AreEqual(false, new DirectoryInfoSample().TryToCreateDirectory(directoryInfoStub));
directoryInfoStub.AssertWasNotCalled(x => x.Create());
}
ファイルシステムをテストする際に、oneする必要は必ずしもありません。実際には、状況に応じて、いくつかのことができます。
あなたが尋ねる必要がある質問は:私は何をテストしていますか?
ファイルシステムが機能するかどうかおそらく、テストする必要はありませんthatオペレーティングシステムを使用している場合を除きます「非常に不慣れです。たとえば、単にファイルを保存するコマンドを指定するだけの場合、実際に保存することを確認するテストを作成するのは時間の無駄です。
ファイルが適切な場所に保存されるということですか?さて、どのように適切な場所がわかりますか?おそらく、パスとファイル名を組み合わせたコードがあります。これは簡単にテストできるコードです。入力は2つの文字列であり、出力はこれらの2つの文字列を使用して構築された有効なファイルの場所である文字列である必要があります。
ディレクトリから正しいファイルのセットを取得するということですか?ファイルシステムを実際にテストするファイルゲッタークラスのテストを書く必要があるでしょう。ただし、変更しないファイルを含むテストディレクトリを使用する必要があります。また、このテストは統合テストプロジェクトに配置する必要があります。これは、ファイルシステムに依存するため、これは本当の単体テストではないためです。
ただし、取得したファイルで何かをする必要があります。thatテストでは、fakefile-getterクラスの場合。偽物はハードコードされたファイルのリストを返すはずです。 realfile-getterおよびrealfile-processorを使用する場合、どちらがテスト失敗の原因かわかりません。したがって、テストでは、ファイルプロセッサクラスは偽のファイルゲッタークラスを使用する必要があります。ファイルプロセッサクラスは、ファイルゲッターinterfaceを取る必要があります。実際のコードでは、実際のファイルゲッターを渡します。テストコードでは、既知の静的リストを返す偽のファイルゲッターを渡します。
基本的な原則は次のとおりです。
Q1:
ここには3つのオプションがあります。
オプション1:一緒に暮らす
(例なし:P)
オプション2:必要に応じてわずかな抽象化を作成します。
テスト対象のメソッドでファイルI/O(File.ReadAllBytesなど)を実行する代わりに、IOが外部で実行され、代わりにストリームが渡されるように変更できます。
public class MyClassThatOpensFiles
{
public bool IsDataValid(string filename)
{
var filebytes = File.ReadAllBytes(filename);
DoSomethingWithFile(fileBytes);
}
}
になるだろう
// File IO is done outside prior to this call, so in the level
// above the caller would open a file and pass in the stream
public class MyClassThatNoLongerOpensFiles
{
public bool IsDataValid(Stream stream) // or byte[]
{
DoSomethingWithStreamInstead(stream); // can be a memorystream in tests
}
}
このアプローチはトレードオフです。最初に、はい、それはよりテスト可能です。ただし、テスタビリティと引き換えに、複雑さを少し増やします。これは、保守性と記述しなければならないコードの量に影響を与える可能性があり、さらに、テストの問題を1レベル上げることができます。
ただし、私の経験では、完全にラップされたファイルシステムにコミットすることなく、重要なロジックを一般化してテスト可能にすることができるため、これは素晴らしいバランスの取れたアプローチです。つまり残りはそのままにして、本当に気にする部分を一般化できます。
オプション3:ファイルシステム全体をラップする
さらに一歩踏み込んで、ファイルシステムをモックすることは有効なアプローチです。それは、あなたがどれだけ喜んで一緒に暮らすかに依存します。
以前にこのルートに行ったことがあります。ラップされたファイルシステムの実装がありましたが、最終的には削除しました。 APIには微妙な違いがあり、どこにでもそれを注入しなければなりませんでした。そして、それを使用するクラスの多くは私にとってそれほど重要ではなかったので、最終的には少し苦労しました。 IoCコンテナーを使用していたか、重要なものを書いていて、テストを高速にする必要がある場合は、それで固執したかもしれません。これらすべてのオプションと同様に、走行距離は異なる場合があります。
IoCコンテナに関する質問:
二重にテストを手動で注入します。多くの反復作業を行う必要がある場合は、テストでセットアップ/工場メソッドを使用してください。テストにIoCコンテナーを使用すると、極端に過剰になります。ただし、2番目の質問を理解していないのかもしれません。
System.IO.Abstractions
NuGetパッケージを使用します。
このWebサイトには、テストに注入を使用する方法を示す素晴らしい例があります。 http://dontcodetired.com/blog/post/Unit-Testing-C-File-Access-Code-with-SystemIOAbstractions
以下は、Webサイトからコピーされたコードのコピーです。
using System.IO;
using System.IO.Abstractions;
namespace ConsoleApp1
{
public class FileProcessorTestable
{
private readonly IFileSystem _fileSystem;
public FileProcessorTestable() : this (new FileSystem()) {}
public FileProcessorTestable(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public void ConvertFirstLineToUpper(string inputFilePath)
{
string outputFilePath = Path.ChangeExtension(inputFilePath, ".out.txt");
using (StreamReader inputReader = _fileSystem.File.OpenText(inputFilePath))
using (StreamWriter outputWriter = _fileSystem.File.CreateText(outputFilePath))
{
bool isFirstLine = true;
while (!inputReader.EndOfStream)
{
string line = inputReader.ReadLine();
if (isFirstLine)
{
line = line.ToUpperInvariant();
isFirstLine = false;
}
outputWriter.WriteLine(line);
}
}
}
}
}
using System.IO.Abstractions.TestingHelpers;
using Xunit;
namespace XUnitTestProject1
{
public class FileProcessorTestableShould
{
[Fact]
public void ConvertFirstLine()
{
var mockFileSystem = new MockFileSystem();
var mockInputFile = new MockFileData("line1\nline2\nline3");
mockFileSystem.AddFile(@"C:\temp\in.txt", mockInputFile);
var sut = new FileProcessorTestable(mockFileSystem);
sut.ConvertFirstLineToUpper(@"C:\temp\in.txt");
MockFileData mockOutputFile = mockFileSystem.GetFile(@"C:\temp\in.out.txt");
string[] outputLines = mockOutputFile.TextContents.SplitLines();
Assert.Equal("LINE1", outputLines[0]);
Assert.Equal("line2", outputLines[1]);
Assert.Equal("line3", outputLines[2]);
}
}
}
現在、依存性注入を介してIFileSystemオブジェクトを使用しています。実動コードの場合、ラッパークラスはインターフェイスを実装し、特定のIO必要な関数をラップします。テスト時には、nullまたはスタブ実装を作成し、テスト対象のクラスに提供できます。クラスは賢明ではありません。
2012年以降、 Microsoft Fakes を使用してこれを行うことができます。コードベースは既に凍結されているため、コードベースを変更する必要はありません。
最初に 偽のアセンブリを生成する System.dllの場合-またはその他のパッケージを使用し、次に次のように期待される戻り値をモックします。
using Microsoft.QualityTools.Testing.Fakes;
...
using (ShimsContext.Create())
{
System.IO.Fakes.ShimFile.ExistsString = (p) => true;
System.IO.Fakes.ShimFile.ReadAllTextString = (p) => "your file content";
//Your methods to test
}