views:

434

answers:

4

I am writing unit tests for a project in ASP.NET MVC 1.0 using Moq and MvcContrib TestHelper classes. I have run into a problem.

When I come to Roles.AddUserToRole in my AccountController, I get a System.NotSupportedException. The Roles class is static and Moq cannot mock a static class.

What can I do?

+3  A: 

You could use a pattern like DI (Dependency Injection). In your case, I would pass a RoleProvider to the AccountController, which would be the default RoleProvider by default, and a mock object in your tests. Something like:

public class AccountController
{
    private MembershipProvider _provider;
    private RoleProvider roleProvider;

    public AccountController()
      : this(null, null)
    {
    }

    public AccountController(MembershipProvider provider, RoleProvider roleProvider)
    {
      _provider = provider ?? Membership.Provider;
      this.roleProvider = roleProvider ?? System.Web.Security.Roles.Provider;
    }
}

The MVC runtime will call the default constructor, which in turn will initialize the AccountController with the default role provider. In your unit test, you can directly call the overloaded constructor, and pass a MockRoleProvider (or use Moq to create it for you):

[Test]
public void AccountControllerTest()
{
    AccountController controller = new AccountController(new MockMembershipProvider(), new MockRoleProvider());
}

EDIT: And here's how I mocked the entire HttpContext, including the principal user. To get a Moq version of the HttpContext:

public static HttpContextBase GetHttpContext(IPrincipal principal)
{
  var httpContext = new Mock<HttpContextBase>();
  var request = new Mock<HttpRequestBase>();
  var response = new Mock<HttpResponseBase>();
  var session = new Mock<HttpSessionStateBase>();
  var server = new Mock<HttpServerUtilityBase>();
  var user = principal;


  httpContext.Setup(ctx => ctx.Request).Returns(request.Object);
  httpContext.Setup(ctx => ctx.Response).Returns(response.Object);
  httpContext.Setup(ctx => ctx.Session).Returns(session.Object);
  httpContext.Setup(ctx => ctx.Server).Returns(server.Object);
  httpContext.Setup(ctx => ctx.User).Returns(user);

  return httpContext.Object;
}

A mock implementation of Principal:

  public class MockPrincipal : IPrincipal
  {
    private IIdentity _identity;
    private readonly string[] _roles;

    public MockPrincipal(IIdentity identity, string[] roles)
    {
      _identity = identity;
      _roles = roles;
    }

    public IIdentity Identity
    {
      get { return _identity; }
      set { this._identity = value; }
    }

    public bool IsInRole(string role)
    {
      if (_roles == null)
        return false;
      return _roles.Contains(role);
    }
  }

A MockIdentity:

public class MockIdentity : IIdentity
  {
    private readonly string _name;

    public MockIdentity(string userName)    {
      _name = userName;
    }

    public override string AuthenticationType
    {
      get { throw new System.NotImplementedException(); }
    }

    public override bool IsAuthenticated
    {
      get { return !String.IsNullOrEmpty(_name); }
    }

    public override string Name
    {
      get { return _name; }
    }
  }

And the magic call:

MockIdentity identity = new MockIdentity("JohnDoe");
var httpContext = MoqHelpers.GetHttpContext(new MockPrincipal(identity, null));

Note that I edited the code above to leave out some custom stuff, but I'm quite sure this should still work.

Razzie
I still need help with Roles.AddUserToRoleWhen I mock RoleProvider I don't have this method. I have AddUsersToRoles which takes (string[] usernames, string roles[])
LencoTB
Ok, but that has nothing to do with the mocking itself. Either you call the AddUsersToRoles method with 'new string[] { "johndoe" }' as the first parameter or something, or you add the method yourself. I think Roles.AddUserToRole calls AddUsersToRoles underneath anyway, so nothing stops you from doing that yourself!
Razzie
A: 

Now I run into another problem when I try to test the ChangePassword() method in ASP.NET MVC.

        try
        {
            if (MembershipService.ChangePassword(User.Identity.Name, currentPassword, newPassword))
            {
                if (!TempData.ContainsKey("ChangePassword_success"))
                {
                    TempData.Add("ChangePassword_success", true);
                }

                return PartialView("ChangePassword");

            }

Now I get that User is null, when I reach this line. In my testclass I have:

mockMembershipService.Setup(cp => cp.ChangePassword("johndoe", currentPassword, newPassword)).Returns(true);

I thought that this would work, but it doesn't care for that I send "johndoe". And If I were to mock IPrincipal, the User property is readonly.

LencoTB
You will have to mock the HttpContext and the Principal user. The user property does not have to be readonly, but your GenericIdentity name does.
Razzie
A: 

TypeMock Isolator does mocking of statics etc. But I second (and +1'd) Razzie's answer.

Ruben Bartelink
To late to change to TypeMock Isolator for me right now. Can MvcContrib do something to mock IPrincipal or something that sets User.Identity.Name?
LencoTB
see my updated answer.
Razzie
@LencoTB: BTW I Wasnt recommending TMI - I was just stating that it's the only mocking framework that supports mocking statics. In general, not relying on statics is a good thing for lots of other reasons which makes this a moot point.
Ruben Bartelink
A: 

I have done what you coded, but I still get that User is null when it reaches:

mockMembershipService.Setup(cp => cp.ChangePassword("johndoe", currentPassword, newPassword)).Returns(true);

In my Testclass I have:

        //Arrange (Set up a scenario)
        var mockMembershipService = new Mock<IMembershipService>();
        MockIdentity identity = new MockIdentity("JohnDoe");
        var httpContext = MoqHelpers.GetHttpContext(new MockPrincipal(identity, null));
        var controller = new AccountController(null, mockMembershipService.Object, null, null, null);
        string currentPassword = "qwerty";
        string newPassword = "123456";
        string confirmPassword = "123456";

        // Expectations

         mockMembershipService.Setup(pw => pw.MinPasswordLength).Returns(6);
         mockMembershipService.Setup(cp => cp.ChangePassword("johndoe", currentPassword, newPassword)).Returns(true);

Do I call my cp.ChangePassword with wrong parameters? And should MVCContrib Testhelpers classes be able to mock Http context and so on? I just can't find info for how to setup User.Identity.Name with MVCContrib. I have used this from a tutorial to test something (mock) session:

        var builder = new TestControllerBuilder();
        var controller = new AccountController(mockFormsAuthentication.Object, mockMembershipService.Object, mockUserRepository.Object, null, mockBandRepository.Object);
        builder.InitializeController(controller);

EDIT: I have come a little further:

        MockIdentity identity = new MockIdentity("JohnDoe");
        var httpContext = MoqHelpers.GetHttpContext(new MockPrincipal(identity, null));
        var controller = new AccountController(null, mockMembershipService.Object, null, null, null);
        controller.ControllerContext = new ControllerContext(httpContext, new RouteData(), controller);

but my now I can't get my cp.ChangePassword in the expectation to return true:

mockMembershipService.Setup(cp => cp.ChangePassword("johndoe", currentPassword, newPassword)).Returns(true);

I am sending "johndoe" string, because, it requires a string as a parameter for User.Identity.Name, but it doesn't return true.

LencoTB
Why would you get that error at that line? You're not doing anything with the User there, right? Or did you replace "johndoe" with User.Identity.Name on that line? Anyway, you can't use User.Identity.Name there, you should replace it with something like:cp.ChangePassword(new MockIdentity("JohnDoe").Name, currentPassword, newPassword)
Razzie
Ah my bad, I wrote the updated before I got your answer. And now it works when I send new MockIdentity("JohnDoe").Name. Thank you for your help and sorry if these were newbie questions. I am still new to Moq and find it hard to test things from ASP.NET MVC's runtime. MVCContrib TestHelper classes should help, but there is not a lot of documentation on those. So I think I will stick with your mocking of HttpContext and Identity, until I figure out how to do it with MVCContrib.
LencoTB
No problem, and they certainly aren't newbie questions. I'm new to Moq myself. I have no knowledge of the TestHelper classes in the MVCContrib however, but I do feel that Moq is great. And to be fair, it's pretty easy to test stuff that uses the ASP.NET runtime in MVC. However, easy != a lot of work :-) Glad it works!
Razzie