web-dev-qa-db-ja.com

ASP.NET MVC5アプリの単体テスト

新しいプロパティを追加して、ApplicationUserクラスを拡張しています(チュートリアルに示されているように FacebookおよびGoogle OAuth2とOpenIDサインオン(C#)を使用してASP.NET MVC 5アプリを作成します

public class ApplicationUser : IdentityUser
{
    public DateTime BirthDate { get; set; }
}

ここで、UnitControllerを作成して、AccountControllerがBirthDateを正しく保存していることを確認します。

TestUserStoreという名前のメモリ内ユーザーストアを作成しました

[TestMethod]
public void Register()
{
    // Arrange
    var userManager = new UserManager<ApplicationUser>(new TestUserStore<ApplicationUser>());
    var controller = new AccountController(userManager);

    // This will setup a fake HttpContext using Moq
    controller.SetFakeControllerContext();

    // Act
    var result =
        controller.Register(new RegisterViewModel
        {
            BirthDate = TestBirthDate,
            UserName = TestUser,
            Password = TestUserPassword,
            ConfirmPassword = TestUserPassword
        }).Result;

    // Assert
    Assert.IsNotNull(result);

    var addedUser = userManager.FindByName(TestUser);
    Assert.IsNotNull(addedUser);
    Assert.AreEqual(TestBirthDate, addedUser.BirthDate);
}

Controller.Registerメソッドは、MVC5によって生成される定型コードですが、参照のためにここに含めています。

// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser() { UserName = model.UserName, BirthDate = model.BirthDate };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await SignInAsync(user, isPersistent: false);
            return RedirectToAction("Index", "Home");
        }
        else
        {
            AddErrors(result);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Registerを呼び出すと、SignInAsyncが呼び出され、トラブルが発生します。

private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

最下層では、定型コードにこのtidbitが含まれます

private IAuthenticationManager AuthenticationManager
{
    get
    {
        return HttpContext.GetOwinContext().Authentication;
    }
}

これは、問題のルートが発生する場所です。このGetOwinContextの呼び出しは拡張メソッドであり、モックできず、スタブに置き換えることもできません(もちろん定型コードを変更しない限り)。

このテストを実行すると、例外が発生します

Test method MVCLabMigration.Tests.Controllers.AccountControllerTest.Register threw exception: 
System.AggregateException: One or more errors occurred. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at System.Web.HttpContextBaseExtensions.GetOwinEnvironment(HttpContextBase context)
at System.Web.HttpContextBaseExtensions.GetOwinContext(HttpContextBase context)
at MVCLabMigration.Controllers.AccountController.get_AuthenticationManager() in AccountController.cs: line 330
at MVCLabMigration.Controllers.AccountController.<SignInAsync>d__40.MoveNext() in AccountController.cs: line 336

以前のリリースでは、ASP.NET MVCチームはコードをテスト可能にするために一生懸命働いていました。現在、AccountControllerのテストは簡単ではないようです。いくつかの選択肢があります。

できます

  1. 拡張メソッドを呼び出さないようにボイラープレートコードを変更し、そのレベルでこの問題に対処する

  2. テスト目的でOWinパイプラインをセットアップする

  3. AuthN/AuthZインフラストラクチャを必要とするテストコードの記述を避けます(合理的なオプションではありません)

どちらの道が良いかわかりません。どちらでもこれを解決できます。私の質問は、どちらが最良の戦略であるかを要約しています。

注:はい、書いていないコードをテストする必要がないことは知っています。 MVC5が提供するUserManagerインフラストラクチャは、インフラストラクチャの一部ですが、ApplicationUserへの変更を検証するテスト、またはユーザーロールに依存する動作を検証するコードを作成する場合、UserManagerを使用してテストする必要があります。

27
Ron Jacobs

私が自分の質問に答えているので、これが良い答えだと思うなら、コミュニティから感覚を得ることができます。

ステップ1:生成されたAccountControllerを変更して、バッキングフィールドを使用してAuthenticationManagerのプロパティセッターを提供します。

// Add this private variable
private IAuthenticationManager _authnManager;

// Modified this from private to public and add the setter
public IAuthenticationManager AuthenticationManager
{
    get
    {
        if (_authnManager == null)
            _authnManager = HttpContext.GetOwinContext().Authentication;
        return _authnManager;
    }
    set { _authnManager = value; }
}

ステップ2:単体テストを変更して、Microsoft.OWin.IAuthenticationManagerインターフェイスのモックを追加します

[TestMethod]
public void Register()
{
    // Arrange
    var userManager = new UserManager<ApplicationUser>(new TestUserStore<ApplicationUser>());
    var controller = new AccountController(userManager);
    controller.SetFakeControllerContext();

    // Modify the test to setup a mock IAuthenticationManager
    var mockAuthenticationManager = new Mock<IAuthenticationManager>();
    mockAuthenticationManager.Setup(am => am.SignOut());
    mockAuthenticationManager.Setup(am => am.SignIn());

    // Add it to the controller - this is why you have to make a public setter
    controller.AuthenticationManager = mockAuthenticationManager.Object;

    // Act
    var result =
        controller.Register(new RegisterViewModel
        {
            BirthDate = TestBirthDate,
            UserName = TestUser,
            Password = TestUserPassword,
            ConfirmPassword = TestUserPassword
        }).Result;

    // Assert
    Assert.IsNotNull(result);

    var addedUser = userManager.FindByName(TestUser);
    Assert.IsNotNull(addedUser);
    Assert.AreEqual(TestBirthDate, addedUser.BirthDate);
}

これでテストに合格しました。

良いアイデア?悪いアイデア?

27
Ron Jacobs

私のニーズも似ていますが、AccountControllerの純粋な単体テストが必要ないことに気付きました。むしろ、私はそれをその自然の生息地に可能な限り近い環境でテストしたいと思います(必要であれば統合テスト)。したがって、周囲のオブジェクトをモックしたくはありませんが、実際のオブジェクトを使用します。

HttpContextBaseExtensions.GetOwinContextメソッドも邪魔になるので、Bliscoのヒントにとても満足しました。現在、私のソリューションの最も重要な部分は次のようになっています。

/// <summary> Set up an account controller with just enough context to work through the tests. </summary>
/// <param name="userManager"> The user manager to be used </param>
/// <returns>A new account controller</returns>
private static AccountController SetupAccountController(ApplicationUserManager userManager)
{
    AccountController controller = new AccountController(userManager);
    Uri url = new Uri("https://localhost/Account/ForgotPassword"); // the real string appears to be irrelevant
    RouteData routeData = new RouteData();

    HttpRequest httpRequest = new HttpRequest("", url.AbsoluteUri, "");
    HttpResponse httpResponse = new HttpResponse(null);
    HttpContext httpContext = new HttpContext(httpRequest, httpResponse);
    Dictionary<string, object> owinEnvironment = new Dictionary<string, object>()
    {
        {"owin.RequestBody", null}
    };
    httpContext.Items.Add("owin.Environment", owinEnvironment);
    HttpContextWrapper contextWrapper = new HttpContextWrapper(httpContext);

    ControllerContext controllerContext = new ControllerContext(contextWrapper, routeData, controller);
    controller.ControllerContext = controllerContext;
    controller.Url = new UrlHelper(new RequestContext(contextWrapper, routeData));
    // We have not found out how to set up this UrlHelper so that we get a real callbackUrl in AccountController.ForgotPassword.

    return controller;
}

私はまだすべてを機能させることに成功していません(特に、UrlHelperにForgotPasswordメソッドで適切なURLを生成させることができませんでした)が、私のニーズのほとんどは現在カバーされています。

4
Pierre America

私はあなたに似たソリューション-IAuthenticationManagerのモックを使用しましたが、ログインコードはLoginManagerクラスにあり、コンストラクタインジェクションを介してIAuthenticationManagerを取得します。

    public LoginHandler(HttpContextBase httpContext, IAuthenticationManager authManager)
    {
        _httpContext = httpContext;
        _authManager = authManager;
    }

私は nity を使用して依存関係を登録しています:

    public static void RegisterTypes(IUnityContainer container)
    {
        container.RegisterType<HttpContextBase>(
            new InjectionFactory(_ => new HttpContextWrapper(HttpContext.Current)));
        container.RegisterType<IOwinContext>(new InjectionFactory(c => c.Resolve<HttpContextBase>().GetOwinContext()));
        container.RegisterType<IAuthenticationManager>(
            new InjectionFactory(c => c.Resolve<IOwinContext>().Authentication));
        container.RegisterType<ILoginHandler, LoginHandler>();
        // Further registrations here...
    }

ただし、Unityの登録をテストしたいので、これは偽造せずにトリッキーであることがわかりました(a)HttpContext.Current(十分に難しい)および(b)GetOwinContext() 。

Phil Haackの HttpSimulator およびHttpContextを操作して基本的な Owin環境 の形式のソリューションを見つけました。これまでのところ、単一のダミーOwin変数を設定するだけでGetOwinContext()が機能するのに十分であることがわかりましたが、YMMVです。

public static class HttpSimulatorExtensions
{
    public static void SimulateRequestAndOwinContext(this HttpSimulator simulator)
    {
        simulator.SimulateRequest();
        Dictionary<string, object> owinEnvironment = new Dictionary<string, object>()
            {
                {"owin.RequestBody", null}
            };
        HttpContext.Current.Items.Add("owin.Environment", owinEnvironment);
    }        
}

[TestClass]
public class UnityConfigTests
{
    [TestMethod]
    public void RegisterTypes_RegistersAllDependenciesOfHomeController()
    {
        IUnityContainer container = UnityConfig.GetConfiguredContainer();
        HomeController controller;

        using (HttpSimulator simulator = new HttpSimulator())
        {
            simulator.SimulateRequestAndOwinContext();
            controller = container.Resolve<HomeController>();
        }

        Assert.IsNotNull(controller);
    }
}

SetFakeControllerContext()メソッドが機能する場合、HttpSimulatorは使い過ぎかもしれませんが、統合テストに役立つツールのように見えます。

3
Blisco