Home –  未分類 – ASP.NET CoreでIntegration Test

ASP.NET CoreでIntegration Test

ASP.NET Coreを細々と勉強しています。ASP.NET Coreでテストはどうやればいいんだろうと思い調べてみました。
特に、URLベースでアクセスしてそのレスポンスに対して精査をするという事をしようと思います。

今回は公式ドキュメントとか若干記述が足りないところがあり、スタックオーバーフローなどをさまよう羽目になりました。
同じような事で困っている人の役に立てたらうれしいかなと思っています(´・ω・`)

下準備

  • Visual Studio for Mac
  • ソリューションを一つ
    • ASP.NET CoreのWebアプリケーションプロジェクト
    • xUnitのテストプロジェクト

今回はついにリリースされたVisual Studio for Macで作業しましたが、別にWindowsでも変わらないと思います。
各プロジェクトは単純に作成してビルドだけしています。

テストプロジェクト側の設定

プロジェクト画像

テストプロジェクト側には、ASP.NET Coreのプロジェクト(TestApp01)を参照しておきます。
また、パッケージとして「Microsoft.AspNetCore.TestHost」を加えます。

最初のつまづき

当初Integration testing | Microsoft Docsを見ながら作業を行ったのですが、どうしてもうまく行かず四苦八苦しました。
このドキュメントのまま作業をすると

  • appsetting.json関連でエラーが発生
  • View(Razor)関連でエラーが発生

といった現象に悩まされました。

これらを参照したところ無事テストが実行出来るようになりました。

とりあえず動作するソース

色々ごちゃごちゃ細かく書くよりも作ったものを書きたいと思います。

using Xunit;
using System.IO;
using System.Threading.Tasks;
using System.Net.Http;
using System.Reflection;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.PlatformAbstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.CodeAnalysis;
using TestApp01;

namespace TestApp01Tests
{
  public static class WebHostBuilderExtensions
  {
    private static string ContentPath
    {
      get
      {
        var path = PlatformServices.Default.Application.ApplicationBasePath;
        var contentPath = Path.GetFullPath(Path.Combine(path, $@"../../../../{nameof(TestApp01)}"));
        return contentPath;
      }
    }

    public static IWebHostBuilder ConfigureTestContent(this IWebHostBuilder builder)
    {
      return builder.UseContentRoot(ContentPath);
    }

    public static IWebHostBuilder ConfigureTestServices(this IWebHostBuilder builder)
    {
      return builder.ConfigureServices(services =>
      {
        services.AddMvcCore();
        services.Configure((RazorViewEngineOptions options) =>
        {
          var previous = options.CompilationCallback;
          options.CompilationCallback = (context) =>
          {
            previous?.Invoke(context);

            var assembly = typeof(Startup).GetTypeInfo().Assembly;
            var assemblies = assembly.GetReferencedAssemblies()
                         .Select(x => MetadataReference.CreateFromFile(Assembly.Load(x).Location))
                         .ToList();
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("mscorlib")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Private.Corelib")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Html.Abstractions")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Razor")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Razor.Runtime")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Mvc")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Runtime")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Threading.Tasks")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Dynamic.Runtime")).Location));
            assemblies.Add(MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Text.Encodings.Web")).Location));

            context.Compilation = context.Compilation.AddReferences(assemblies);
          };
        });

      });
    }
  }

    public class UnitTest1
    {
    private readonly TestServer _server;
    private readonly HttpClient _client;

    public UnitTest1()
    {
      _server = new TestServer(new WebHostBuilder()
        .UseStartup<Startup>()
          .ConfigureTestContent()
                .ConfigureTestServices()
      );
      _client = _server.CreateClient();


    }

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

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

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

簡単な解説

大半がテスト用のホストの準備です。テスト本体はUnitTest1クラスのTest1メソッドとなります。

UnitTest1クラスのコンストラクタでテスト用のホストとクライアント作成し、それを利用してテストを行います。
コンストラクタでConfigureTestContentConfigureTestServicesが呼ばれています。

これらが上の方で定義しているWebHostBuilderExtensionsクラスで拡張メソッドとして定義されています。

ConfigureTestContent拡張メソッドについて

テストプロジェクトでそのままホストを起動すると、コンテンツのルートディレクトリの位置が変わってしまいます。
そのために、appsetting.jsonファイルなどが読めずにエラーとなります

この拡張メソッドではそのコンテンツルートディレクトの正しい位置を設定するという機能を持っています。

ConfigureTestServicesメソッドについて

上のConfigureTestContent拡張メソッドでコンテンツのディレクトリを正しく設定したとしてもViewの描画でエラーが発生します。
View上で使用している各種クラスがみつからないようです。
.NET初心者でいまいちよくわかっていないのですが、このメソッドにより各クラスのアセンブリを読み込ませる事によりエラーが発生しなくなるということだと思います。

そんなわけで

無事テスト出来るようになったんですが、View関連はもう少しスマートになりませんでしょうかね。。。個人的にテストフレームワーク使ってテストってほとんどやったことないので、何もかもが勉強でした(;・∀・)

コメントを残す