ASP.NET Core MVC 2.0 時点での xUnit と Microsoft.AspNetCore.TestHost を利用した Integration Test で気をつける点

ここ数年 C# からは離れていたのですが久しぶりに触ることになったのでテスト周りがどうなっているのか調べている途中で TestHost に出会いました。

## Microsoft.AspNetCore.TestHost を使った Integration Test に関して

[ASP.NET Core でのテストの統合 | Microsoft Docs](https://docs.microsoft.com/ja-jp/aspnet/core/testing/integration-testing)

自動翻訳で微妙に読みにくいですが、上記が一つのポインタになると思います。

RSpec だと request spec 辺りになるかと思います。feature spec のようなブラウザにレンダリングさせての End-to-End テストとは異なりますね。

Web API を作っているケースで使えそうという感じです。

## 最も簡単なテスト

これは上に貼ったリンクのサンプルです。

以下の `Startup` はテスト対象の ASP.NET Core アプリケーション側のクラスです。WebHost としてビルドしたものを　`TestServer` に渡しているだけですね。

後は、そこから作成できるクライアントでテストを書いていけます。

```c#
public class PrimeWebDefaultRequestShould
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    public PrimeWebDefaultRequestShould()
    {
        // Arrange
        _server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>());
        _client = _server.CreateClient();
    }

    [Fact]
    public async Task ReturnHelloWorld()
    {
        // Act
        var response = await _client.GetAsync("/");
        response.EnsureSuccessStatusCode();

        var responseString = await response.Content.ReadAsStringAsync();

        // Assert
        Assert.Equal("Hello World!",
            responseString);
    }
}
```

### MVC の場合

上記のドキュメントの中盤の「Mvc Razor/テストの統合」にもあるように以下の設定をテスト側の csproj に追加しなくてはいけません。

```xml:HogeTest.csproj
<PropertyGroup>
  <PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
```

ただ、これだけだと動作しません。

- [ASP.NET CoreでIntegration Test | トコトコ日記](http://www2.gyafuuuun.com/2017/05/asp-net-core%E3%81%A7integration-test/)
- [asp.net mvc - Razor compilation fails on TestHost when rendering view xUnit tests - Stack Overflow](https://stackoverflow.com/questions/48243151/razor-compilation-fails-on-testhost-when-rendering-view-xunit-tests)
- [Creating instance of TestServer throws exception after upgrading to VS 2017 · Issue #959 · aspnet/Hosting](https://github.com/aspnet/Hosting/issues/959#issuecomment-286351703)
- [Integration tests with ASP.NET Core causes missing references from Razor files · Issue #1212 · aspnet/Razor](https://github.com/aspnet/Razor/issues/1212)
- [How do I solve a "view not found" exception in asp.net core mvc project - Stack Overflow](https://stackoverflow.com/questions/37529040/how-do-i-solve-a-view-not-found-exception-in-asp-net-core-mvc-project)

上記のようにいろいろと workaround があるようですが、たいてい以下のことをやっています。

- `shadowCopy: false` を設定した `xunit.runner.json` を配置
- deps.json をコピーするビルドタスクを追加

```xml:HogeTest.csproj
  <Target Name="CopyAditionalFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
    <ItemGroup>
      <DepsFilePaths Include="$([System.IO.Path]::ChangeExtension('%(_ResolvedProjectReferencePaths.FullPath)', '.deps.json'))" />
    </ItemGroup>

    <Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutputPath)" Condition="Exists('%(DepsFilePaths.FullPath)')" />
  </Target>
```

- テスト対象のプロジェクトを ContentRoot とするように `UseContentRoot` を指定

```c#
var asm = typeof(Startup).Assembly.GetName().Name;
string appRootPath = Path.GetFullPath(Path.Combine(
    AppContext.BaseDirectory,
    "..", "..", "..", "..", asm));

_server = new TestServer(new WebHostBuilder()
    .UseContentRoot(appRootPath) // 💡 これを追加
    .UseStartup<Startup>());
```

### 2.1 でその辺りがよくなる

[Improved end to end testing support for MVC applications · Issue #275 · aspnet/Announcements](https://github.com/aspnet/Announcements/issues/275)

ここでケアされるのは以下のようです。

> - It copies the .deps file from your project into the test assembly bin folder.
- It sets the content root the application's project root so that static files and views can be found.
- It provides a class WebApplicationTestFixture<TStartup> that streamlines the bootstrapping of your app on
TestServer.

上記には無い xunit でのシャドウコピーに関しては引き続き行う必要があると書いてありますね。

内容については以下らへんで実際に見ることができます。（すでに preview も出ているので試すこともできるのかな）

https://github.com/aspnet/Mvc/tree/dev/src/Microsoft.AspNetCore.Mvc.Testing

`WebApplicationTestFixture` の実装や、`deps.json`のコピーに関する build target ファイルもありますね。

上記に加えて MVC 側から `MvcTestFixture` が提供されるようです。

https://github.com/aspnet/Mvc/blob/dev/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/MvcTestFixture.cs

### サンプル

以下に今回検証したサンプルを置いておきます。

> （ホントは E2E テストじゃないんですが、最初は E2E テストが書きたくて試し始めたので名が体を表してないのは許してください）

https://github.com/dany1468/aspnetcore2_0_TestHost_sample

`E2eTestSample` は Visual Studio for Mac で MVC プロジェクトを作ってそのままです。

`E2eTest` は以下のようになっています。

- TestUsingWebHostBuilderDirectly.cs
  - workaround の実装の通りに `UseContentRoot` を使ったもの
- TestUsingInfrastracture.cs
  - `infrastructure/` にコピーしてきた、2.1 で追加されるものを利用して書き直したもの

`infrastructure` ディレクトリのファイルは、`dev` ブランチから今回のお試しに必要なものを取ってきただけです。`WebHostBuilderExtensions` は以下の `aspnet/Hosting` の一部です。

https://github.com/aspnet/Hosting/blob/dev/src/Microsoft.AspNetCore.TestHost/WebHostBuilderExtensions.cs

## その他 ASP.NET Core 2.0 での　Integration Test の参考になりそうなもの

- [API Series Part 6 - ASP.NET Core 2.0 and Integration Testing — Jack Vanlightly](https://jack-vanlightly.com/blog/2017/10/8/api-series-part-6-aspnet-core-20-and-integration-testing)
- [Testing an ASP.NET Core application using TestServer - Meziantou's blog](https://www.meziantou.net/2017/08/21/testing-an-asp-net-core-application-using-testserver)
- [donbavand/dotnetcore-integration-tests-example: Create a simple test integration framework in .NET Core 2.0](https://github.com/donbavand/dotnetcore-integration-tests-example)
- [Integration Testing in .NET Core 2.0](https://dev.to/donbavand/integration-testing-in-net-core-20)
- [Introduction to integration testing with xUnit and TestServer in ASP.NET Core](https://andrewlock.net/introduction-to-integration-testing-with-xunit-and-testserver-in-asp-net-core/)