Home –  ASP.NET Core
Tag Archives: ASP.NET Core

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関連はもう少しスマートになりませんでしょうかね。。。個人的にテストフレームワーク使ってテストってほとんどやったことないので、何もかもが勉強でした(;・∀・)

ASP.NET Core で複雑なバリデーションをする

本日もメモがてら・・・

ASP.NET Coreのバリデーションはモデルにアノテーションを入れるだけでサックサクに出来るので楽させてもらっています。
最大文字数制御や正規表現による入力規制とかは大丈夫なのですが、色々な要素が絡みあった入力チェックをしたいときがあります。

別テーブルのデータを見てとある入力がある場合は入力を特定の値に制限するなど、別テーブルまで絡んだりすると標準のバリデーションでは出来ないのではないかと思います。

ASP.NET CoreではIValidatableObjectを実装したクラスに対してValidation時にValidateのメソッドを呼び出してくれます。
そこでモデル全体としての入力チェックが出来るので、そこで行うと良いみたいです。

この時にDbContext(ApplicationDbContext)が使いたくなることもあると思います。その場合はvalidationContext.GetServiceで取得することが可能です。
データベースに以外にもサービスが定義されていれば必要に応じて取得できそうです。

using System.ComponentModel.DataAnnotations;

public class HogeHoge : IValidatableObject
{

    // 〜〜〜 色々省略 〜〜

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        //DbContext取得
        var dbContext = (ApplicationDbContext)validationContext.GetService(typeof(ApplicationDbContext));


        if(true){ //本当は入力チェック(;・∀・)
            yield return new ValidationResult("エラーメッセージ");
        }

    }
}

EntityFramework Core(EFCore)で更新日時を自動的に入れたい

あとで忘れないようにφ(..)メモメモ

EntityFrameworkでデータを更新した際に自動でデータを生成・保存したい項目ってありますよね。
更新日時とか更新ユーザー名とか。

更新日時とかはRailsであればcreated_atとか項目作っておけば勝手にやってくれますが、
EntityFrameworkでは違うみたいです。

色々調べたところDBContextのSaveChangesをオーバーライドして、保存処理が走るまえにデータをいれて上げるのが良いみたいです。

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{

    //〜〜〜〜〜〜〜〜省略〜〜〜〜

    public override int SaveChanges()
    {

        var now = DateTime.Now; //毎度Now取り出すとコストが掛かるのと、更新日時がレコードによって異ならないように。
        var changes = ChangeTracker.Entries<Kokyaku>().Where(p => p.State == EntityState.Modified || p.State == EntityState.Added).Select(u => u.Entity);
        foreach (var change in changes)
        {
            change.UpdDate = now;
        }
        return base.SaveChanges();
    }

}

こんな感じのロジックでKokyakuクラスに関しては、UpdDateフィールドに自動的に更新日時が入るようになります。Kokyakuの部分を親クラスにして、親クラスにUpdDateのフィールドを作るというのが一般的な流れになりそうです。

Webアプリケーション等で更新ユーザーを保存する場合、SignManagerやUserManagerをDIして現在のログインユーザーを保存・・・・と行きたいのですが、A circular dependency was detected for the service of type ‘Microsoft.AspNetCore.Identity.UserManager
などとDIが循環していると怒られてしまいます。

更新ユーザー名を保存するにはもうActionFilterを利用するなど少し仕組みを考える必要がありそうです。