web-dev-qa-db-ja.com

ASP.Net Core 2.0 SignInAsyncが例外を返す値はnullにできません、プロバイダー

ユニットテスト(NUnitを使用)で改造しているASP.Net Core 2.0 Webアプリケーションがあります。アプリケーションは正常に動作し、これまでのほとんどのテストは正常に動作します。

ただし、認証/承認のテスト(ユーザーがログインして_[Authorize]_フィルター操作にアクセスできるかどうか)は失敗します...

_System.ArgumentNullException: Value cannot be null.
Parameter name: provider
_

...後...

_await HttpContext.SignInAsync(principal);
_

...しかし、実際には根本的な原因は何かは明らかではありません。ここで呼び出されたメソッドでコードの実行が停止し、IDEに例外は表示されませんが、コードの実行は呼び出し元に戻り、終了します(まだ出力にThe program '[13704] dotnet.exe' has exited with code 0 (0x0).が表示されます) VSのウィンドウ)

テストエクスプローラーが赤く表示され、参照されている例外が表示されます(それ以外の場合、問題についてはわかりません)

私は人々にポイントするための再現の作成に取り組んでいます(これまでに少し関与したことが判明しました)。

誰かが根本的な原因を特定する方法を知っていますか?これはDI関連の問題ですか(テストでは提供されないが、通常は実行されている必要があります)?

UPDATE1:要求された認証コードを提供しています...

_public async Task<IActionResult> Registration(RegistrationViewModel vm) {
    if (ModelState.IsValid) {
        // Create registration for user
        var regData = createRegistrationData(vm);
        _repository.AddUserRegistrationWithGroup(regData);

        var claims = new List<Claim> {
            new Claim(ClaimTypes.NameIdentifier, regData.UserId.ToString())
        };
        var ident = new ClaimsIdentity(claims);
        var principal = new ClaimsPrincipal(ident);

        await HttpContext.SignInAsync(principal); // FAILS HERE

        return RedirectToAction("Welcome", "App");
    } else {
        ModelState.AddModelError("", "Invalid registration information.");
    }

    return View();
}
_

失敗したテストコード...

_public async Task TestRegistration()
{
    var ctx = Utils.GetInMemContext();
    Utils.LoadJsonData(ctx);
    var repo = new Repository(ctx);
    var auth = new AuthController(repo);
    auth.ControllerContext = new ControllerContext();
    auth.ControllerContext.HttpContext = new DefaultHttpContext();

    var vm = new RegistrationViewModel()
    {
        OrgName = "Dev Org",
        BirthdayDay = 1,
        BirthdayMonth = "January",
        BirthdayYear = 1979 
    };

    var orig = ctx.Registrations.Count();
    var result = await auth.Registration(vm); // STEPS IN, THEN FAILS
    var cnt = ctx.Registrations.Count();
    var view = result as ViewResult;

    Assert.AreEqual(0, orig);
    Assert.AreEqual(1, cnt);
    Assert.IsNotNull(result);
    Assert.IsNotNull(view);
    Assert.IsNotNull(view.Model);
    Assert.IsTrue(string.IsNullOrEmpty(view.ViewName) || view.ViewName == "Welcome");
}
_

UPDATE3:chat @nkosi Suggested に基づいて、これは私が満たしていないことに起因する問題であるHttpContextの依存関係注入要件の必要性。

ただし 、まだ明確になっていないのは、実際に適切なサービス依存関係を提供できないという問題である場合、なぜコードが正常に機能するのか(テストされていない場合)です。 SUT(コントローラー)はIRepositoryパラメーターのみを受け入れます(これがすべての場合に提供されるすべてです)。プログラムの実行時に呼び出される既存のctorがすべてである場合に、テストのためだけにオーバーロードされたctor(またはモック)を作成する理由問題なく動作しますか?

UPDATE4:@Nkosiがバグ/問題に解決策を示しながら答えたものの、なぜIDEでないのか疑問に思っています。 t根本的な例外を正確に/一貫して提示します。これはバグですか、それともasync/awaitオペレーターとNUnitテストアダプター/ランナーが原因ですか?テストのデバッグ中に予期したとおりに例外が「ポップ」せず、終了しますコードはまだゼロです(通常、正常な戻り状態を示します)?

16
t.j.

まだ明確になっていないのは、実際には、適切なサービス依存関係を提供できないという問題である場合、なぜコードが正常に機能するのか(テストされていない場合)です。 SUT(コントローラー)は、IRepositoryパラメーターのみを受け入れます(これがすべての場合に提供されるすべてです)。プログラムの実行時に呼び出される既存のctorがすべてである場合に、テストのためだけにオーバーロードされたctor(またはモック)を作成する理由問題なく動作しますか?

ここではいくつかのことを混同しています。まず第一に、個別のコンストラクタを作成する必要はありません。テスト用ではなく、アプリケーションの一部として実際に実行するためでもありません。

コントローラがパラメータとしてコンストラクタに持つすべての直接的な依存関係を定義する必要があります。これにより、これがアプリケーションの一部として実行されると、依存関係注入コンテナがそれらの依存関係をコントローラに提供します。

ただし、これも重要なポイントです。アプリケーションを実行するときに、オブジェクトの作成と必要な依存関係の提供を担当する依存関係注入コンテナがあります。したがって、実際にはそれらがどこから来たのかについてあまり心配する必要はありません。ただし、単体テストの場合は異なります。単体テストでは、依存関係の注入は使用しないでください。これは、依存関係を隠蔽するだけなので、テストと競合する可能性のある副作用です。ユニットテスト内の依存性注入に依存していることは、ユニットテストではなく、統合を行っているという非常に良い兆候です代わりにテストします(少なくとも実際にDIコンテナーをテストしている場合を除きます)。

代わりに、単体テストでは、すべての依存関係を明示的に提供するすべてのオブジェクトexplicitlyを作成します。これは、コントローラーを新しくして、コントローラーが持つすべての依存関係を渡すことを意味します。理想的には、モックを使用して、単体テストで外部の動作に依存しないようにします。

ほとんどの場合、これはかなり簡単です。残念ながら、コントローラーには特別なものがあります。コントローラーには、MVCライフサイクル中に自動的に提供されるControllerContextプロパティがあります。 MVC内の他のいくつかのコンポーネントには同様のものがあります(たとえば、ViewContextも自動的に提供されます)。これらのプロパティは、注入されたコンストラクタではないため、依存関係は明示的に表示されません。コントローラーの機能によっては、コントローラーの単体テスト時にこれらのプロパティも設定する必要がある場合があります。


単体テストでは、コントローラーアクション内でHttpContext.SignInAsync(principal)を使用しているため、残念ながらHttpContextを直接操作しています。

SignInAsyncは拡張メソッドであり、 基本的に次のことを行います

context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties);

したがって、このメソッドは、便宜上、 サービスロケータパターン を使用して、依存関係注入コンテナからサービスを取得し、サインインを実行します。したがって、HttpContextに対するこの1つのメソッド呼び出しだけで、テストが失敗した場合にのみ検出されるimplicit依存関係がさらに取り込まれます。これは、 サービスロケーターパターンを回避する必要がある理由(= /// =)の良い例として役立つはずです 。 –しかし、ここでは、これは便利な方法であるため、これに対応し、テストを調整してこれで動作するようにする必要があります。

実際、次に進む前に、ここで良い代替ソリューションについて触れたいと思います。コントローラーはAuthControllerなので、その中核的な目的の1つは、認証やユーザーのサインインやサインインなどを行うことだと想像できます。そのため、実際にはHttpContext.SignInAsyncを使用せずに、IAuthenticationService明示的な依存関係としてコントローラーに設定し、その上で直接メソッド。そうすることで、テストで満たすことができる明確な依存関係が得られ、サービスロケーターに関与する必要がなくなります。

もちろん、これはこのコントローラの特別なケースであり、HttpContextの拡張メソッドのevery可能な呼び出しに対しては機能しません。それでは、これを適切にテストする方法に取り組みましょう。

コードからSignInAsyncが実際に何をしているのかを見ることができるので、HttpContext.RequestServicesIServiceProviderを提供し、IAuthenticationServiceを返すことができるようにする必要があります。これらを模擬します:

var authenticationServiceMock = new Mock<IAuthenticationService>();
authenticationServiceMock
    .Setup(a => a.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
    .Returns(Task.CompletedTask);

var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock
    .Setup(s => s.GetService(typeof(IAuthenticationService)))
    .Returns(authenticationServiceMock.Object);

次に、コントローラーを作成した後、ControllerContextでそのサービスプロバイダーを渡すことができます。

var controller = new AuthController();
controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext()
    {
        RequestServices = serviceProviderMock.Object
    }
};

HttpContext.SignInAsyncを機能させるために必要なのはこれだけです。

残念ながら、もう少しあります。 この他の答え (すでに見つけました)で説明したように、ユニットテストでRedirectToActionResultを設定している場合、コントローラーからRequestServicesを返すと問題が発生します。 RequestServicesはnullではないため、RedirectToActionの実装はIUrlHelperFactoryを解決しようとし、その結果はnull以外である必要があります。そのため、モックを少し拡張して、モックも提供する必要があります。

var urlHelperFactory = new Mock<IUrlHelperFactory>();
serviceProviderMock
    .Setup(s => s.GetService(typeof(IUrlHelperFactory)))
    .Returns(urlHelperFactory.Object);

幸い、他に何もする必要はありません。また、ファクトリーモックにロジックを追加する必要もありません。そこにあれば十分です。

これで、コントローラのアクションを適切にテストできます。

// mock setup, as above
// …

// arrange
var controller = new AuthController(repositoryMock.Object);
controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext()
    {
        RequestServices = serviceProviderMock.Object
    }
};

var registrationVm = new RegistrationViewModel();

// act
var result = await controller.Registration(registrationVm);

// assert
var redirectResult = result as RedirectToActionResult;
Assert.NotNull(redirectResult);
Assert.Equal("Welcome", redirectResult.ActionName);

IDEが正確に/一貫して根本的な例外を提示していないのはなぜですか?これはバグですか、それともasync/awaitオペレーターとNUnitテストアダプター/ランナーが原因ですか?

非同期テストでも過去に似たようなものを見たことがあります。それらを正しくデバッグできなかったり、例外が正しく表示されなかったりすることです。最近のバージョンのVisual StudioとxUnitでこれを見たことを覚えていません(私は個人的には、NUnitではなくxUnitを使用しています)。それが役立つ場合、dotnet testを使用してコマンドラインからテストを実行すると、通常は正常に動作し、障害の適切な(非同期)スタックトレースを取得できます。

14
poke

これはDI関連の問題ですか(テストでは提供されていないが、通常は実行されている必要があります)?

[〜#〜]はい[〜#〜]

実行時にフレームワークがセットアップする機能を呼び出しています。分離された単体テスト中に、これらを自分で設定する必要があります。

コントローラのHttpContextには、IServiceProviderを解決するために使用するIAuthenticationServiceがありません。そのサービスは実際にSignInAsyncを呼び出します

させるために....

_await HttpContext.SignInAsync(principal);  // FAILS HERE
_

...Registrationアクションでユニットテスト中に完了するまで実行するには、SignInAsync拡張メソッドが失敗しないように、サービスプロバイダーをモックする必要があります。

単体テストの配置を更新する

_//...code removed for brevity

auth.ControllerContext.HttpContext = new DefaultHttpContext() {
    RequestServices = createServiceProviderMock()
};

//...code removed for brevity
_

ここで、createServiceProviderMock()は、_HttpContext.RequestServices_の入力に使用されるサービスプロバイダーのモックに使用される小さなメソッドです

_public IServiceProvider createServiceProviderMock() {
    var authServiceMock = new Mock<IAuthenticationService>();
    authServiceMock
        .Setup(_ => _.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
        .Returns(Task.FromResult((object)null)); //<-- to allow async call to continue

    var serviceProviderMock = new Mock<IServiceProvider>();
    serviceProviderMock
        .Setup(_ => _.GetService(typeof(IAuthenticationService)))
        .Returns(authServiceMock.Object);

    return serviceProviderMock.Object;
}
_

また、コントローラーのアクションの分離された単体テストの目的でRepositoryをモックして、マイナスの影響なしに完了まで流れることを確認することをお勧めします

2
Nkosi

@pokeはユニットテストで依存性注入を使用せず、依存関係を明示的に(モックを使用して)提供しない方がよいですが、統合テストでこの問題があり、問題がRequestServicesHttpContextプロパティで発生することがわかりました。テスト(テストで実際のHttpContextを使用しないため)ので、以下のようにHttpContextAccessorを登録し、必要なすべてのサービスを(手動で)渡し、問題を解決しました。以下のコードを参照してください

Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });

私はそれがあまりクリーンな解決策ではないことに同意しますが、アプリケーションIHttpContextAccessorHttpContextで必要なHttContext依存関係(テストメソッドで自動的に提供されなかった)を提供するために、テストでのみこのコードを記述して使用したことに注意してください。フレームワークによって自動的に提供されます。

テストの基本クラスコンストラクターにあるすべての依存関係登録メソッドは次のとおりです

 public class MyTestBaseClass
 {
  protected ServiceCollection Services { get; set; } = new ServiceCollection();
  MyTestBaseClass
 {

   Services.AddDigiTebFrameworkServices();
        Services.AddDigiTebDBContextService<DigiTebDBContext> 
        (Consts.MainDBConnectionName);
        Services.AddDigiTebIdentityService<User, Role, DigiTebDBContext>();
        Services.AddDigiTebAuthServices();
        Services.AddDigiTebCoreServices();
        Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });
}
}
1
Code_Worm