Nov 2, 2016

Testing an MVC site with integrations - part 1

Just recently I became a Dynamics CRM integration consultant. That's a new product for me, in the past I have done one integration, but it was just a small data syncronization thing, and not a "real" project.

I joined an existing project, where the product is an Orchard (an ASP.NET MVC 5 based CMS system) module that integrates with a Dynamics CRM installation. It provides a self-service point for the client's customers. Sounds good, eh? Orchard was new to me, in addition to Dynamics CRM, but I quickly found my way around. Orchard uses Autofac, log4net and Automapper internally, so it's quick and easy to hook up your work using their conventions.

Anyway, this blog post was intended to be about testing. When I joined the project, there were literally no tests in there. So I started diving in to write some, before doing changes to the code and scratching my head, wondering if things were still working as intended.

It quickly became apparent why there were no tests... At least from my point of view, the only tests that could be written at that point, were end-to-end tests. MVC controllers were calling Orchard and CRM proxy code here and there directly, and a lot of information was being passed to other classes in their constructors, not to mention a lot of work was carried out in these constructors.

As we all know, or should know by now, tight coupling is bad for you. So my first mission became to reduce the coupling between Orchard and our Orchard module to a minimum. Sounds like a strange thing to do? I don't think so, and all you need to do is really to place any Orchard-referencing code in an apt interface. There, now you can mock all your user settings, content and what not, and actually write unit tests on your controllers (or other classes, of course).

And the same applies to the CRM bits of course. Extract interfaces that do the CRM integration bits. In my case, this made sense to put in a separate library. The project actually has several different clients, so gathering the common logic in this library saves time. Now that your controller only references some interfaces that we can mock - we can start writing actual tests!

So let's begin by testing the routes in your application. In my case I found a few routes that lead nowhere, and removed them, I also found a few not working and fixed them. That's value from writing the first ten or so tests... If you're using MVC there's a neat helper function in the MvcContrib.TestHelper package. It let's you write a route test like this:

"sample/12345".Route().ShouldMapTo<SampleController>( x => x.Action("12345"));

If your project is something other than a "vanilla" MVC application, you may be providing routes in some exotic ways, other than adding them to the RouteTable. If so, remember you can still add your routes to the default RouteTable in your test setup method.

In Orchard, you can get the routes from your route provider and use them in tests like so:

var routes = new MyPage.Routes().GetRoutes().ToReadOnlyCollection();
routes.ForEach(s => RouteTable.Routes.Add(s.Route));

Second, I test the validation on the input models. I'm talking about the dataannotation-type validation, with attributes on the model's properties. It does not cover your coded validation of your model. This is important both for your data integrity and security reasons, but also for the users experience when using the application. If validation fails, the user may be left unable to perform whatever task she is up to (worst case: offering you a job). The easiest way I find to test this validation, is by going directly to the ValidationContext that MVC uses internally:

private IList<ValidationResult> ValidateModel(object model) {
var validationResults = new List<ValidationResult>();
var ctx = new ValidationContext(model, null, null);
Validator.TryValidateObject(model, ctx, validationResults, true);
return validationResults;
}

You can simply check if the validationResult.Count > 0 if you want to see if the model passed validation. If it is a specific valdiation error you are looking for, don't just check that it is greater than zero - assert that it is exactly the number expected, and with the expected error:

Assert.That(result.ContainsKey("InvalidPostCode"));

Third, we can start testing the controllers. We now know that the routes will resolve to the correct controller and action, time to test whether the action returns the expected result. Again, the MvcContrib.Testhelper library helps us get going:

var builder = new TestControllerBuilder();
var controller = Container.Resolve<YourController>();
builder.InitializeController(controller);

Now our controller variable has all the properties that your code expects to find on it, and you can test TempData, Forms variables, HttpContext etc.

That should be enough for this first part, I hope to write some more soon :)

No comments:

Post a Comment