Mocking a simple service bus in ASP.NET MVC
Asked Answered
O

1

7

I have a simple 'Service' system set up with an interface as shown below. I am trying to mock it for use in my unit testing, but am having a bit of an obstacle. The way it works is that I design classes that implement IRequestFor<T,R> and I would call the service bus like this...

var member = new Member { Name = "[email protected]", Password = "validPassword" }; ServiceBus.Query<ValidateUser>().With(member);

This works fine in my code. I have no issues with it. But when I try to mock it, like this ..

var service = Mock.Create<IServiceBus>();

            // Model
            var model = new Web.Models.Membership.Login
            {
                Email = "[email protected]",
                Password = "acceptiblePassword",
                RememberMe = true
            };

            // Arrange
            Mock.Arrange(() => service.Query<Membership.Messages.ValidateMember>().With(model))
                .Returns(true);

I am given the following error.

NullReferenceException

I don't even know what the exception is on. It 'points' to the ServiceBus in my Controller code, and if I use the debugger, the object is like .. {IServiceBus_Proxy_2718486e043f432da4b143c257cef8ce}, but other than that, everything else looks the exact same as if I step through it in a normal run.

I am using Telerik JustMock for the mocking, but I don't know how I would do this in a different mocking framework either. I am using Ninject for my Dependency Injection, as well. Can anyone help me?

For convenience, I have included as much of my code as possible below.

Code Reference

Service Bus

public interface IServiceBus
{
    T Query<T>() where T : IRequest;
    T Dispatch<T>() where T : IDispatch;
}

public interface IRequest
{
}

public interface IDispatch
{

}

public interface IRequestFor<TResult> : IRequest
{
    TResult Reply();
}

public interface IRequestFor<TParameters, TResult> : IRequest
{
    TResult With(TParameters parameters);
}

public interface IDispatchFor<TParameters> : IDispatch
{
    void Using(TParameters parameters);
}

Service Bus Implementation

public class ServiceBus : IServiceBus
{
    private readonly IKernel kernel;

    public ServiceBus(IKernel kernel) {
        this.kernel = kernel;
    }

    /// <summary>
    /// Request a query behavior that may be given parameters to yield a result.
    /// </summary>
    /// <typeparam name="T">The type of query to request.</typeparam>
    /// <returns></returns>
    public T Query<T>() where T : IRequest
    {
        // return a simple injected instance of the query.
        return kernel.Get<T>();
    }

    /// <summary>
    /// Request a dispatch handler for a given query that may be given parameters to send.
    /// </summary>
    /// <typeparam name="T">The type of handler to dispatch.</typeparam>
    /// <returns></returns>
    public T Dispatch<T>() where T : IDispatch
    {
        // return a simple injected instance of the dispatcher.
        return kernel.Get<T>();
    }
}

Service Bus Dependency Injection Wiring (Ninject)

Bind<IServiceBus>()
                .To<ServiceBus>()
                .InSingletonScope();

Complete Unit Test

    [TestMethod]
    public void Login_Post_ReturnsRedirectOnSuccess()
    {
        // Inject
        var service = Mock.Create<IServiceBus>();
        var authenticationService = Mock.Create<System.Web.Security.IFormsAuthenticationService>();

        // Arrange
        var controller = new Web.Controllers.MembershipController(
            service, authenticationService
        );

        var httpContext = Mock.Create<HttpContextBase>();

        // Arrange
        var requestContext = new RequestContext(
            new MockHttpContext(),
            new RouteData());

        controller.Url = new UrlHelper(
            requestContext
        );

        // Model
        var model = new Web.Models.Membership.Login
        {
            Email = "[email protected]",
            Password = "acceptiblePassword",
            RememberMe = true
        };

        // Arrange
        Mock.Arrange(() => service.Query<Membership.Messages.ValidateMember>().With(model))
            .Returns(true);

        // Act
        var result = controller.Login(model, "/Home/");

        // Assert
        Assert.IsInstanceOfType(result, typeof(RedirectResult));
    }

Actual Query Method

public class ValidateMember : IRequestFor<IValidateMemberParameters, bool>
{
    private readonly ISession session;

    public ValidateMember(ISession session) {
        this.session = session;
    }

    public bool With(IValidateMemberParameters model)
    {
        if (String.IsNullOrEmpty(model.Email)) throw new ArgumentException("Value cannot be null or empty.", "email");
        if (String.IsNullOrEmpty(model.Password)) throw new ArgumentException("Value cannot be null or empty.", "password");

        // determine if the credentials entered can be matched in the database.
        var member = session.Query<Member>()
            .Where(context => context.Email == model.Email)
            .Take(1).SingleOrDefault();

        // if a member was discovered, verify their password credentials
        if( member != null )
            return System.Security.Cryptography.Hashing.VerifyHash(model.Password, "SHA512", member.Password);

        // if we reached this point, the password could not be properly matched and there was an error.
        return false;
    }
}

Login Controller Action

    [ValidateAntiForgeryToken] 
    [HttpPost]
    public ActionResult Login(Web.Models.Membership.Login model, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            // attempt to validate the user, and if successful, pass their credentials to the
            // forms authentication provider. 
            if (Bus.Query<ValidateMember>().With(model))
            {
                // retrieve the authenticated member so that it can be passed on
                // to the authentication service, and logging can occur with the
                // login.
                Authentication.SignIn(model.Email, model.RememberMe);

                if (Url.IsLocalUrl(returnUrl))
                    return Redirect(returnUrl);
                else
                    return RedirectToAction("Index", "Home");
            }
            else
            {
                ModelState.AddModelError("", "The user name or password provided is incorrect.");
            }
        }

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

Login View Model

public class Login : Membership.Messages.IValidateMemberParameters
{
    [Required]
    [DataType(DataType.EmailAddress)]
    [RegularExpression(@"^[a-z0-9_\+-]+(\.[a-z0-9_\+-]+)*@(?:[a-z0-9-]+){1}(\.[a-z0-9-]+)*\.([a-z]{2,})$", ErrorMessage = "Invalid Email Address")]
    [Display(Name = "Email Address")]
    public string Email { get; set; }

    [Required]
    [StringLength(32, MinimumLength = 6)]
    [DataType(DataType.Password)]
    [RegularExpression(@"^([a-zA-Z0-9@#$%]){6,32}$", ErrorMessage = "Invalid Password. Passwords must be between 6 and 32 characters, may contain any alphanumeric character and the symbols @#$% only.")]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }
}

enter image description here

Opia answered 14/4, 2011 at 12:45 Comment(0)
P
2

I don't have any real experience with how JustMock works in terms of recursive/nested mocking, but looking at the documentation it may look like that kind of mocking works only if your intermediate chain members are properties. And you're trying to implicitly mock IServiceBus method, which is generic, what can be an obstacle, too.

Mock.Arrange(() => service.Query<Membership.Messages.ValidateMember>().With(model))
            .Returns(true); 

You want to set the expectation here on With method from ValidateMember, assuming that the Query<T> method on IServiceBus will be mocked automatically, which may not be a case.

What should work here is to mock it more "traditionally", with two steps - first mock your Query<T> method on IServiceBus to return a mock of ValidateMember, which you should mock to return true.

var validateMemberMock = Mock.Create<Membership.Messages.ValidateMember>();
Mock.Arrange(() => service.Query<Membership.Messages.ValidateMember>())
            .Returns(validateMemberMock);
Mock.Arrange(() => validateMemberMock.With(model))
            .Returns(true); 

EDIT Here's my passing code doing more less the same what yours:

[TestClass]
public class JustMockTest
{
    public interface IServiceBus
    {
        T Query<T>() where T : IRequest;
    }

    public interface IRequest
    {
    }

    public interface IRequestFor<TParameters, TResult> : IRequest
    {
        TResult With(TParameters parameters);
    }

    public class ValidateMember : IRequestFor<IValidateMemberParameters, bool>
    {
        public bool With(IValidateMemberParameters model)
        {
            return false;
        }
    }

    public class MembershipController
    {
        private IServiceBus _service;

        public MembershipController(IServiceBus service)
        {
            _service = service;
        }

        public bool Login(Login model)
        {
            return _service.Query<ValidateMember>().With(model);
        }
    }

    public interface IValidateMemberParameters
    {

    }

    public class Login : IValidateMemberParameters
    {
        public string Email;
        public string   Password;
        public bool RememberMe;
    }

    [TestMethod]
    public void Login_Post_ReturnsRedirectOnSuccess()
    {
        // Inject
        var service = Mock.Create<IServiceBus>();

        // Arrange
        var controller = new MembershipController(service);

        // Model
        var model = new Login
        {
            Email = "[email protected]",
            Password = "acceptiblePassword",
            RememberMe = true
        };

        var validateMemberMock = Mock.Create<ValidateMember>();
        Mock.Arrange(() => service.Query<ValidateMember>())
                    .Returns(validateMemberMock);
        Mock.Arrange(() => validateMemberMock.With(model)).IgnoreArguments()
                    .Returns(true); 

        // Act
        var result = controller.Login(model);

        // Assert
        Assert.IsTrue(result);
    }
}
Pye answered 16/4, 2011 at 21:37 Comment(10)
This gets me a little further. Now I get the error on the ISession instead of on the IServiceBus. I'm adding a screenshot of it to my post.Opia
Now it looks like the mock arrangement on ValidateMember is not working at all. It shouldn't call the real With implementation as it should be mocked to return true immediately. I'll dig for more info in a moment.Pye
Well, I've downloaded JustMock's most recent version, pasted all your code and run the test successfully. If you're sure you're mocking it correctly, maybe you're working with an older version?Pye
You can also try to ignore model argument value for call to With method (Mock.Arrange(() => validateMemberMock.With(model)).IgnoreArguments().Returns(true);). Maybe there's something wrong with your Login class equality and the mock treats the "act" call as the call with different argument?Pye
I have added my Login action to the text, right above the picture. The error does happen on the Act part of the test, so you may be onto something.Opia
The only thing you haven't shown yet is Web.Models.Membership.Login class. If you show this, I'll be almost running your code. And it pass for me. I'm using JustMock in version 2011.1.315.0 with VS2010 and .NET 4.0. I've pasted my code if you want to compare.Pye
I have added my Login view model class per your request.Opia
Even with your code, I am still getting errors in the With method from the ISession. It just is not going around that and I can't figure out why...Opia
If I remove the session.Query part of the ValidateMember class, the test runs and passes. So I have narrowed the problem down to that... still not sure why it fails though. As you said, it should not actually call that method.Opia
Still no luck. Your theory is sound and solid but for whatever reason it just doesn't work. The only way I have been able to get my test to pass is to remove the actual query in the real code.Opia

© 2022 - 2024 — McMap. All rights reserved.