Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking a simple service bus in ASP.NET MVC

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

like image 779
Ciel Avatar asked Apr 14 '11 12:04

Ciel


1 Answers

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);
    }
}
like image 148
NOtherDev Avatar answered Nov 15 '22 00:11

NOtherDev