Unit Testing ASP.NET MVC5 App

冷暖自知 提交于 2019-11-29 19:53:59
Ron Jacobs

I'm answering my own question so I can get a sense from the community if you think this is a good answer.

Step 1: Modify the generated AccountController to provide a property setter for the AuthenticationManager using a backing field.

// 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; }
}

Step 2: Modify the unit test to add a mock for the Microsoft.OWin.IAuthenticationManager interface

[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);
}

Now the test passes.

Good idea? Bad idea?

My needs are similar, but I realized that I don't want a pure unit test of my AccountController. Rather I want to test it in an environment that is as close as possible to its natural habitat (integration test, if you want). So I don't want to mock the surrounding objects, but use the real ones, with as little of my own code as I can get away with.

The HttpContextBaseExtensions.GetOwinContext method also got in my way, so I was very happy with Blisco's hint. Now the most important part of my solution looks like this:

/// <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;
}

I have not yet succeeded to get everything working (in particular, I could not get UrlHelper to produce a proper URL in the ForgotPassword method), but most of my needs are covered now.

I've used a solution similar to yours - mocking an IAuthenticationManager - but my login code is in a LoginManager class that takes the IAuthenticationManager via constructor injection.

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

I'm using Unity to register my dependencies:

    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...
    }

However, I'd like to test my Unity registrations, and this has proved tricky without faking (a) HttpContext.Current (hard enough) and (b) GetOwinContext() - which, as you've found, is impossible to do directly.

I've found a solution in the form of Phil Haack's HttpSimulator and some manipulation of the HttpContext to create a basic Owin environment. So far I've found that setting a single dummy Owin variable is enough to make GetOwinContext() work, but 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);
    }
}

HttpSimulator may be overkill if your SetFakeControllerContext() method does the job, but it looks like a useful tool for integration testing.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!