ファイルを簡単にテストできるようになる、System.IO.Abstractionsの紹介

# 概要

テスト自動化をするうえで、ファイルの検索・読込・書込は鬼門です。テストによって環境が変化することも、環境によってテスト結果が変化してしまうのも望ましくありません。

テスト時のみ一時ファイルに切り替えてもよいですが、それでもストレージへのアクセスがあるとテスト時間が長くなってしまいます。

そこで、ファイルシステムのモックを簡単につくれる、System.IO.Abstractionsを紹介します。

# ファイルの内容を捜査するアプリ

## 修正前アプリケーションコード

[公式ReadMe](https://github.com/System-IO-Abstractions/System.IO.Abstractions)のサンプルを少し変更した以下のようなアプリを題材にします。

```csharp
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("思想捜査開始");
        var myComp = new MyComponent();
        myComp.Validate();
    }
}

public class MyComponent
{
    public void Validate()
    {
        foreach (var textFile in System.IO.Directory.GetFiles(@"D:\TestFolder", "2+2.txt", System.IO.SearchOption.TopDirectoryOnly))
        {
            var text = System.IO.File.ReadAllText(textFile);
            if (text != "5")
            {
                System.IO.File.WriteAllText(textFile, "5");
                throw new NotSupportedException("BIG BROTHER IS WATCHING YOU");
            }
        }
    }
}
```

あるフォルダの中から"2+2.txt"というファイルを探して、中の答えが**間違っていたら**修正して警告するアプリケーションです。

しかしこの`MyComponent`クラスはテストする上で以下の問題があります。

- 2+2.txt"ファイルを特定のディレクトリに用意する必要があります。もしそのディレクトリが作れない環境だとテストもできない
- "2+2.txt"ファイルの中身が**間違っている**ケースのテストは実行するたびにファイルが**正しく**されるので、2回目からは失敗する
- テストのたびにストレージにアクセスするので、少し時間がかかる 
- **間違った**中身の"2+2.txt"ファイルを実際に自分のPCに置くのは身の危険を感じる

そこで、System.IO.Abstractionsを使って、この問題を解決します。

## 修正後アプリケーションコード

nugetで以下の2つを入れます。

```console
dotnet add package System.IO.Abstractions
dotnet add package System.IO.Abstractions.TestingHelpers
```

先ほどのコードを修正します。

```csharp
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("思想捜査開始");
        var myComp = new MyComponentAb(new System.IO.Abstractions.FileSystem());
        myComp.Validate();
    }
}

public class MyComponentAb
{
    private IFileSystem fileSystem;

    public MyComponentAb(System.IO.Abstractions.IFileSystem fileSystem)
    {
        this.fileSystem = fileSystem;
    }

    public void Validate()
    {
        foreach (var textFile in fileSystem.Directory.GetFiles(@"D:\TestFolder", "2+2.txt", System.IO.SearchOption.TopDirectoryOnly))
        {
            var text = fileSystem.File.ReadAllText(textFile);
            if (text != "5")
            {
                fileSystem.File.WriteAllText(textFile, "5");
                throw new NotSupportedException("BIG BROTHER IS WATCHING YOU");
            }
        }
    }
}
```

`MyComponentAb`のコンストラクタで、実際のファイルシステムかモックかを切り替えます。コンソールアプリケーション側からは当然実際のファイルシステム`System.IO.Abstractions.FileSystem()`を入れています。
動作は変わりません。

## テストコード

次に`MyComponentAb`のテストを作ります。テストライブラリはxUnitを使いますが、他のテストライブラリでもたいして変わらないと思います。

ソリューションにxUnitテストプロジェクトを追加します。テストしたいプロジェクトの参照を追加して、以下のnugetを追加します。

```console
dotnet add package System.IO.Abstractions
dotnet add package System.IO.Abstractions.TestingHelpers
```

そしてテストコードを追加します。

```csharp
public class UnitTest1
{
    private string targetFilePath = @"D:\TestFolder\2+2.txt";

    [Fact]
    public void Test_MyComponent_Success()
    {
        var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>()
        {
            [targetFilePath] = new MockFileData("5")
        });

        var myComp = new MyComponentAb(fileSystem);

        myComp.Validate();
    }

    [Fact]
    public void Test_MyComponent_ExceptionThrow()
    {
        var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>()
        {
            [targetFilePath] = new MockFileData("4")
        });

        var myComp = new MyComponentAb(fileSystem);

        Assert.Throws<NotSupportedException>(() => myComp.Validate());

        Assert.Equal("5",
            fileSystem.File.ReadAllText(targetFilePath));
    }
}
```

**正しい**ファイルと**間違った**ファイルの2つのテストを作り、両者とも成功しました。モックファイルシステムは`Dictionary`の形で保持しています。ファイルの修正が正しくできていることもモックから確認できます。

![スクリーンショット 2021-07-03 163149.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/d11560f4-eb1a-dabb-10f9-f3c80f0d8f67.png)

System.IO.Abstractionsはこれ以外にもFileSystemWatcherやFileInfoなどのモックがあります。ぜひ使ってみてください。

# 参考

https://github.com/System-IO-Abstractions/System.IO.Abstractions

# 全体コード
https://github.com/soi013/DemoIOAbstractions/

# 環境
VisualStudio 2019
C# 9
.NET 5
Microsoft.NET.Test.Sdk 16.9.4 
xunit 2.4.1 
xunit.runner.visualstudio 2.4.3 
coverlet.collector 3.0.2 
System.IO.Abstractions 13.2.38 
System.IO.Abstractions.TestingHelpers 13.2.38