Question

Writing unit tests that require database access via my CustomMembershipProvider.

edit -

 public class CustomMembershipProvider : MembershipProvider
    {
         public override bool ValidateUser(string username, string password)
        {

            using (var usersContext = new UsersContext())
            {
                var requiredUser = usersContext.GetUser(username, password);
                var userApproved = usersContext.GetUserMem(username);
                if (userApproved == null) return false;
                return (requiredUser != null && userApproved.IsApproved != false);
            }
        }
    }

   [TestFixture]
    public class AccountControllerTest
    {

        [Test]
        public void ShouldNotAcceptInvalidUser()
        {
            // OPTION1
            Mock<IMembershipService> membership = new Mock<IMembershipService>();
            //OPTION2
            // Mock<AccountMembershipService> membership = new Mock<AccountMembershipService>();

            membership.Setup(m => m.ValidateUser(It.IsAny<string>(), It.IsAny<string>()))
                      .Returns(false);
            var logonModel = new LoginModel() { EmailorUserName = "connorgerv", Password = "pasdsword1" };
            var controller = new AccountController(membership.Object);

            // Act
            var result = controller.Login(logonModel,"Index") as RedirectResult;

            // Assert
            Assert.That(result.Url, Is.EqualTo("Index"));
            Assert.False(controller.ModelState.IsValid);
            Assert.That(controller.ModelState[""],
                        Is.EqualTo("The user name or password provided is incorrect."));
        }

        [Test]
        public void ExampleForMockingAccountMembershipService()
        {
            var validUserName = "connorgerv";
            var validPassword = "passwordd1";
            var stubService = new Mock<CustomMembershipProvider>();
            bool val = false;

            stubService.Setup(x => x.ValidateUser(validUserName, validPassword)).Returns(true);

            Assert.IsTrue(stubService.Object.ValidateUser(validUserName, validPassword));
        }

    }



public class AccountController : Controller
    {
        public IMembershipService MembershipService { get; set; }

        public AccountController(IMembershipService service){

            MembershipService=service;
        }

        protected override void Initialize(RequestContext requestContext)
        {
            if (MembershipService == null) { MembershipService = new AccountMembershipService(); }

            base.Initialize(requestContext);
        }

        public ActionResult Index()
        {
            return RedirectToAction("Profile");
        }


        public ActionResult Login()
        {
            if (User.Identity.IsAuthenticated)
            {
                //redirect to some other page
                return RedirectToAction("Index", "Home");
            }
            return View();
        }

        //
        // POST: /Account/Login

        [HttpPost]
        public ActionResult Login(LoginModel model, string ReturnUrl)
        {
            if (ModelState.IsValid)
            {
                if (MembershipService.ValidateUser(model.EmailorUserName, model.Password))
                {

                    SetupFormsAuthTicket(model.EmailorUserName, model.RememberMe);

                    if (Url.IsLocalUrl(ReturnUrl) && ReturnUrl.Length > 1 && ReturnUrl.StartsWith("/")
                        && !ReturnUrl.StartsWith("//") && !ReturnUrl.StartsWith("/\\"))
                    {
                        return Redirect(ReturnUrl);
                    }
                    return RedirectToAction("Index", "Home");
                }
                ModelState.AddModelError("", "The user name or password provided is incorrect.");
            }

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


  public class AccountMembershipService : IMembershipService
    {
        private readonly MembershipProvider _provider;

        public AccountMembershipService()
            : this(null)
        {
        }

        public AccountMembershipService(MembershipProvider provider)
        {
            _provider = provider ?? Membership.Provider;
        }


        public virtual bool ValidateUser(string userName, string password)
        {
            if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
            if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password");

            return _provider.ValidateUser(userName, password);
        }

    }

Membership in web.config of main application

<membership defaultProvider="CustomMembershipProvider">
  <providers>
    <clear />
    <add name="CustomMembershipProvider" type="QUBBasketballMVC.Infrastructure.CustomMembershipProvider" connectionStringName="UsersContext" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10" applicationName="/" />
  </providers>
</membership>

 public class CustomMembershipProvider : MembershipProvider
{
     public override bool ValidateUser(string username, string password)
    {

        using (var usersContext = new UsersContext())
        {
            var requiredUser = usersContext.GetUser(username, password);
            var userApproved = usersContext.GetUserMem(username);
            if (userApproved == null) return false;
            return (requiredUser != null && userApproved.IsApproved != false);
        }
    }
}

What happens when I run ShouldNotAcceptInvalidUser() with Option1 uncommented I can see that MembershipService is a Mock<IMembershipService> in the AccountController but it never steps into MembershipService.ValidateUser on the login Action.

When I run with option2 uncommented the same thing happens except MembershipService is Mock<AccountMembershipService> in the accountcontroller and it hits the AccountMembership Contstructor with null parameters, which in turn sets is to SqlMembershipProvider as Membership.Provider is System.Web.Security.SqlMembershipProvider

Also ExampleForMockingAccountMembershipService() doesn't seem to hit the ValidateUsermethod at all in CustomMembershipProvider and always returns true.

Hopefully this is enough to see where i'm going wrong!! :/

Was it helpful?

Solution

Thanks for providing your code. I think I have a much better handle on what you're trying to do now.

For your ShouldNotAcceptInvalidUser test, you should definitely mock IMembershipService instead of AccountMembershipService (choose option 1 over option 2). Since your controller is your SUT, it should be the only "real" class in the test, in order to minimize the number of moving parts.

With option 1, there's no reason to expect that MembershipService.ValidateUser would step into any code. The MembershipService is a mock object - and you've explicitly told it to just always return false when that method is called. Based on the code here, and using option 1, I'd expect this test to pass.

In your other test, ExampleForMockingAccountMembershipService, you're mocking your SUT which is something you should not do. Your SUT should be the only "real" object in your test. That means all collaborating objects should be mocked, leaving the SUT to be the only object doing anything meaningful. (That way, if the test fails, you know for sure that it's because of a bug in the SUT.)

(Note: ValidateUser was always returning true here because you mocked the SUT, and explicitly told it to always return true. This is why it's never a good idea to mock your SUT - mocking changes the behavior that you're trying to test.)

Based on the code you provided, I'm guessing that the reason you mocked CustomMembershipProvider is because it doesn't fully implement its abstract base class MembershipService. If this is indeed the case, then you will need to implement the missing methods manually, instead of relying on the mocking framework to provide default implementations.

Here is what I believe you were intending this test to look like:

    [Test]
    public void ExampleForMockingAccountMembershipService()
    {
        var validUserName = "connorgerv";
        var validPassword = "passwordd1";
        var sut = new CustomMembershipProvider();

        Assert.IsTrue(sut.ValidateUser(validUserName, validPassword));
    }

Something to look out for here is the fact that CustomMembershipProvider instantiates one of its dependencies: UsersContext. In a unit test, since CustomMembershipProvider is your SUT, you'd want to mock all of its dependencies. In this situation, you could use dependency injection to pass an object responsible for creating this dependency (e.g., an IUsersContextFactory), and use a mock factory and context in your test.

If you don't want to go that route, then just be aware that your test could fail because of a bug in CustomMembershipProvider or a bug in UsersContext.

So, the general logic in your tests is sound; the problems mainly stem from confusion on the role of mock objects in your tests. It's kind of a tough concept to get at first, but here are some resources that helped me when I was learning this:

"Dependency Injection in .Net" by Mark Seemann

"Test Doubles" by Martin Fowler

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top