Unit testing Umbraco surface controllers isn't straightforward, due to issues with mocking or faking dependencies. There has been some discussion about it and there are some workarounds regarding using certain test base classes. But in general it's not an easy thing to do, at least at the moment.
Another approach (or workaround) for this is to move the thing you are trying to test outside of the controller and into another class - that itself depends only on standard ASP.Net MVC. Test that instead, leaving your controller so simple that in itself there remains little value in testing it directly.
As an example, I had this to test:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult SignUp(NewsletterSignUpModel model) { if (ModelState.IsValid) { // Model valid so sign-up email address var result = _newsletterSignUpService.SignUp(model.Email, model.FirstName, model.LastName); TempData["SignUpResult"] = result; return RedirectToCurrentUmbracoPage(); } return CurrentUmbracoPage(); }
Pretty simple registration form. Maybe not much value in testing really, but as I've found these can get quite complex with a lot of logic around validation, age checks, CAPTCHAs etc. So it seems worthwhile to be able to do this.
You can see it has a dependency on a "NewsletterSignUpService" which does the actual signing up of the user (to MailChimp in this case). An instance of this service is provided to the controller via the constructor using dependency injection.
In order to test this, I modified the code to create another class that contains much of this logic - the only difference is it returns a boolean result rather than the Umbraco custom ActionResults such as RedirectToCurrentUmbracoPage:
public class NewsletterSignUpPageControllerCommandHandler : INewsletterSignUpPageControllerCommandHandler { private readonly INewsletterSignUpService _newsletterSignUpService; public NewsletterSignUpPageControllerCommandHandler(INewsletterSignUpService newsletterSignUpService) { _newsletterSignUpService = newsletterSignUpService; } public bool HandleSignUp(NewsletterSignUpModel model, ModelStateDictionary modelState, TempDataDictionary tempData) { if (modelState.IsValid) { // Model valid so sign-up email address var result = _newsletterSignUpService.SignUp(model.Email, model.FirstName, model.LastName); tempData["SignUpResult"] = result; return true; } return false; } }
You can see the dependency on the service is now in here, and I can then change my controller to depend instead on this new class:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult SignUp(NewsletterSignUpModel model) { if (_newsletterSignUpCommandHandler.HandleSignUp(model, ModelState, TempData)) { return RedirectToCurrentUmbracoPage(); } return CurrentUmbracoPage(); }
And having done that I can then write the tests on the command handler class (mocking the dependent service layer to return the responses I want to simulate), e.g.:
#region Newsletter sign-up tests [TestMethod] public void NewsletterSignUpPageCommandHandler_SignUpPostInvalid_ReturnsFalse() { // Arrange var model = new NewsletterSignUpModel { Email = string.Empty, }; var modelStateDictionary = new ModelStateDictionary(); modelStateDictionary.AddModelError("Email", "Email is required."); var tempDataDictionary = new TempDataDictionary(); var handler = new NewsletterSignUpPageControllerCommandHandler(MockNewsletterSignUpService()); // Act var result = handler.HandleSignUp(model, modelStateDictionary, tempDataDictionary); // Assert Assert.IsFalse(result); Assert.AreEqual(0, tempDataDictionary.Keys.Count); } [TestMethod] public void NewsletterSignUpPageCommandHandler_SignUpPostValid_ReturnsTrue() { // Arrange var model = new NewsletterSignUpModel { Email = "fred@test.com", }; var modelStateDictionary = new ModelStateDictionary(); var tempDataDictionary = new TempDataDictionary(); var handler = new NewsletterSignUpPageControllerCommandHandler(MockNewsletterSignUpService()); // Act var result = handler.HandleSignUp(model, modelStateDictionary, tempDataDictionary); // Assert Assert.IsTrue(result); Assert.IsNotNull(tempDataDictionary["SignUpResult"]); Assert.AreEqual(NewsletterSignUpResult.Success, (NewsletterSignUpResult)tempDataDictionary["SignUpResult"]); } [TestMethod] public void NewsletterSignUpPageCommandHandler_SignUpPostValidButAlreadySignedUp_ReturnsTrue() { // Arrange var model = new NewsletterSignUpModel { Email = "fred2@test.com", }; var modelStateDictionary = new ModelStateDictionary(); var tempDataDictionary = new TempDataDictionary(); var handler = new NewsletterSignUpPageControllerCommandHandler(MockNewsletterSignUpService()); // Act var result = handler.HandleSignUp(model, modelStateDictionary, tempDataDictionary); // Assert Assert.IsTrue(result); Assert.IsNotNull(tempDataDictionary["SignUpResult"]); Assert.AreEqual(NewsletterSignUpResult.FailedAlreadySignedUp, (NewsletterSignUpResult)tempDataDictionary["SignUpResult"]); } #endregion #region Mocks private static INewsletterSignUpService MockNewsletterSignUpService() { var mock = new Mock<INewsletterSignUpService>(); mock.Setup(x => x.SignUp(It.IsAny<string>(), It.IsAny(), It.IsAny ())).Returns(NewsletterSignUpResult.Success); mock.Setup(x => x.SignUp(It.Is<string>(y => y == "fred2@test.com"), It.IsAny (), It.IsAny ())).Returns(NewsletterSignUpResult.FailedAlreadySignedUp); return mock.Object; } #endregion
Quite nice I think - means we can have testable code, and also follows the good design principles of having "thin" controllers with work delegated to small, defined classes.
Comments
Post a Comment