Unit Testing Views in Spring MVCTesting, Java, Spring, and MVC
Unit testing should not be reserved for compiled code in web projects. The view layer should also be subjected to similar low-level testing to prove that it does what what you expect. This post shows how this can be achieved using Spring MVC.
We're big on testing at Betfair. Personally, I don't think that should be a surprise... what should be a surprise is any serious software developer that ISN'T. But one of the areas of development often neglected in being suitably tested is views. By 'views', I mean the templates that you use to render your user interface components. We test that our classes behave as we expect, so why don't we test that our views (you know... the bits our users actually see) behave as we expect? Maybe you do, maybe you don't... but if you don't, you should!
The main principle here is to write a test that leverages the MVC pattern in much the same way as the real application, but exploits the power of standard unit testing practices (like mocking, assertions and verification) to ensure that the rendered view is what we expect when rendered using a known model. Following the BDD style of testing (like we do):
- a certain model
- I render the view with that model
- I expect the rendered HTML content to conform to some predictable result
How does it work?
We use TestNG for our unit and integration tests, which is supported by Spring via the spring-test artifact; but I was quite surprised when I found that Spring doesn't provide a mechanism for creating an integration test within a
WebApplicationContext. By extending
AbstractTestNGSpringContextTests you get access to an
ApplicationContext, but there's (currently) no way to configure this to return a web application context. Fortunately the source is freely available and the changes are minimal, so I wrote an implementation that tackles this issue quite well (with help from the world wide web, of course).
Integration Test? I thought you said Unit Test?
Yes, that can be a source of confusion - but I really do mean unit test. It just so happens that we can use the infrastructure that Spring provides via it's integration testing features in order to execute the unit test. Think of it this way... if you want to unit test a Java class, you instantiate the class (perhaps with some mocks), and then you invoke operations on that instance. Views can't be instantiated in the same way as Java classes, but require some plumbing to make it happen - enter Spring's integration testing capabilities. But even though we're using integration testing capabilities, the unit testing principles still apply. We're using the plumbing of the view rendering functionality within Spring to prepare and execute our unit test, but we're still testing the view in isolation. The model we provide should contain mocks (and whatever else you would normally provide in the setup of a unit test). It should not rely on any external behaviour that, if changed, would break our test. We're also using those same mocks (and other stuff) to verify that particular operations were invoked (or not) as expected or that the rendered output is valid with respect to the input given. This is a unit test.
Enough yakking... show me the code!
Fortunately there's barely any code, so that won't take long. This is actually quite a trivial concept to implement as most of the hard work is done for you. I've created a very simple demo web application to show how this might work and hosted it on GitHub, so head over there and check it out if you just want to dive straight in. I've structured this in a way that allows the framework to be separately packaged, as in our case we provide this as part of our web platform for all applications to build upon. So we use it both internally within the platform we build and our users (customer-facing products) also use it to test their views too.
Here's a slightly more detailed explanation for those that want it - including some background on design decisions that I'd be very grateful to receive feedback on.
One of the issues I grappled with is how to structure a test. There are loads of options here, and I'm still not convinced that I've found the best one. Fortunately this is still in its infancy, so it's easy to change. Given that we use TestNG, I wanted to leverage some of the cool features it provides - like factories and data providers. I started off down this route, but soon found that it was just needlessly introducing complexity into what should be simple. Admittedly I may have been doing it wrong - so other suggestions are more than welcome.
After seeing this complexity, I removed the factories and data providers and was left with a single test base class that provides the actual test. The only thing each test must do is implement two methods:
then(Document). As I mentioned earlier, we use the BDD style of testing, so this seemed like a natural way to express what these methods do. The base class contains the test, which is basically just a template method that does the test-specific setup, renders the view, then invokes the test-specific assertions & verifications.
In the example above, the MVC context declared in
@ContextConfiguration is referenced to include the
ViewResolver that is used by the application. The view testing framework does not make any restrictions on the view rendering technology chosen, provided it can operate outside of a servlet container. This test would work equally well using either FreeMarker or Velocity (for example), provided that the appropriate
ViewResolver was specified in the application context.
You'll also notice that we're using the Jsoup Document for the
then(..) method, as that provides a nice way of dealing with the DOM. Obviously this won't work if you're using your templates to render non-HTML content (it's a seriously minor change to just return a String containing the rendered view).
To keep the test as simple (and fast) as possible, it is recommended that the application context be limited to the minimal definition of beans that are required for rendering.
- It MUST include the
ViewResolverand any associated configuration required to render a view (this SHOULD be the same configuration you use in your application if you want to test the view as it would appear when running for real).
- It MAY also include the definition of a
MessageSourceif you prefer to test your actual localised text instead of the internationalised capability (more on this in the next section).
- It MUST NOT include a
LocaleResolver, as this would conflict with the
LocaleResolverdefined for use within the test framework (as we are not testing an incoming request, we cannot reliably use any
LocaleResolver- so this is handled internally by the test framework to ensure correct locale resolution)
Most other bean definitions are not specifically related to view-rendering and should be defined elsewhere. By defining your application contexts in this way, you can compose multiple contexts together as and when required but also use them in limited scope when required. For view unit testing, you do not need (and should probably not define) any beans beyond the view-rendering beans specified above. Other code that facilitates view rendering - e.g. instances of classes passed into the model to be used by your template - should be mocked to avoid introducing an unnecessary dependency within the test. This means that the behaviour of the template can be verified independent of the behaviour of the referenced classes.
Many applications make use of Spring's localisation capabilities to provide a site suitable for multiple locales, but this often raises the question about how to test that functionality. I've seen many tests that rely on the translated values within resource bundles, but these prove to be extremely fragile and tend to break frequently (well, at least as frequently as the translations change). Testing in this way also means you need to update your tests when the template you're testing hasn't actually changed - which should ring warning bells!
My preferred approach to testing translations is to test that the application has been internationalised, not each and every localisation. This is a very important distinction that is lost on many people. To avoid confusion, here's a brief definition of each:
Internationalisation is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localisation is the process of adapting internationalised software for a specific region or language by adding locale-specific components and translating text.shamelessly copied from Wikipedia
This approach to view unit testing is built in as the default mechanism (which can be overridden if you really want to). If you choose to use this default approach (strongly recommended) then all you need to do is include assertions that check for the presence of resource bundle keys. Take a look at the
fwk-context.xml file in GitHub and you'll see the
MessageSource has been configured to always return the code that was supplied (no basenames provided and useCodeAsDefaultMessage=true). If you don't get the appropriate key back, you certainly won't get localised text in any locale! If you do get the key back, you can be certain that the appropriate text will be displayed. Whether or not the text in the resource bundle is correct is an entirely separate issue.
Is that all?
Yes, that's all. Please go any play around with this and send your feedback. I'd particularly like to know whether you've done a similar thing in your projects and how your approach differs. Also, it'd be great to get some feedback on the test design... like I said, I'm sure it can be improved - I've just been staring at it too long!
Update: I forgot to mention in the original publishing of this post that some of the ideas were derived from a post by Ted Young. Somehow the reference got lost when I was writing up. But thanks to Ted for his ideas that helped me get this working.