ユニットテスト(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テストアダプター/ランナーが原因ですか?テストのデバッグ中に予期したとおりに例外が「ポップ」せず、終了しますコードはまだゼロです(通常、正常な戻り状態を示します)?
まだ明確になっていないのは、実際には、適切なサービス依存関係を提供できないという問題である場合、なぜコードが正常に機能するのか(テストされていない場合)です。 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.RequestServices
にIServiceProvider
を提供し、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
を使用してコマンドラインからテストを実行すると、通常は正常に動作し、障害の適切な(非同期)スタックトレースを取得できます。
これは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
をモックして、マイナスの影響なしに完了まで流れることを確認することをお勧めします
@pokeはユニットテストで依存性注入を使用せず、依存関係を明示的に(モックを使用して)提供しない方がよいですが、統合テストでこの問題があり、問題がRequestServices
のHttpContext
プロパティで発生することがわかりました。テスト(テストで実際のHttpContextを使用しないため)ので、以下のようにHttpContextAccessor
を登録し、必要なすべてのサービスを(手動で)渡し、問題を解決しました。以下のコードを参照してください
Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });
私はそれがあまりクリーンな解決策ではないことに同意しますが、アプリケーションIHttpContextAccessor
、HttpContext
で必要な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() } });
}
}