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 = "valid@email.com", 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 = "acceptible@email.com",
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 = "acceptible@email.com",
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 i开发者_如何转开发n 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; }
}
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 = "acceptible@email.com",
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);
}
}
精彩评论