We left off with our unit test setup, going over how mock frameworks work and a general pattern for writing tests. Today I’ll review our integration test setup, specifically for web projects. I’m going to focus a little bit on selenium since we use it to simulate a user’s activity on our web application, but a lot of this will apply to most integration tests.
An integration test is a test that spans multiple classes and usually touches one or more “endpoints”. I define an endpoint as an external system that your application doesn’t directly control. Common examples include a database server, a user’s browser, the user interface, the operating system, the file system, a web service, a network resource, and others. The main idea with an integration test is to ensure your system can deal with one complete “action”, such as a user changing their password or uploading a file.
As with unit tests, you’re going to want to use a similar given-when-then pattern for writing these tests: given a state of the system (user starts on the “change password” page), when something happens (the user enters a new password and clicks ‘Change’), then we expect this to happen (the database record is updated, the user is redirected to the home page).
Integration Test Setup
One of the important things with testing is starting from a specific state. With unit tests, you can do this with mock objects fairly easily. With integration tests, even though you’re interacting with external systems that you may not have direct control over, you should still be able to set them up – this may include inserting database rows or making web services calls to create new users for the purpose of your test. This gives two benefits – you can be sure that the data you’re testing against is there and valid (or invalid, depending on what you’re testing), and you know exactly what you should be getting back and can assert against it. As was mentioned before, asserting that you got “something” is much less valuable than asserting that you got a specific object with specific values.
In some cases, there might not be an easy way to create some of your test setup without doing a bunch of manual work. You have two choices: you can do the manual work in a reusable way, or you can bypass the manual stuff and go directly to the data layer of the endpoint you’re working with. As an example, in our project we rely on a WCF service that another team maintains. Instead of asking them to create the endpoints that we need just to setup our integration test, our test setup just talks directly to that service’s database to insert the state we need. After that, we use the service as normal and it returns the data we just inserted. Of course, this is all done in the test environment (never in live!), and you need to make sure you’re manually inserting proper data in case you end up breaking the test service for everyone.
But sometimes you don’t have the luxury of directly database access, and you do have to do a lot of manual work in order to create the application state you want to test against. Believe me, it’s worth it – having a predictable state now saves tons of debugging and “why doesn’t this test work?” questions in the future, and increases developer confidence in tests which is a good thing.
Simulating UI Input
Simulating a user’s input on an interface is probably one of the harder things you have to do writing unit tests, and one of the most important. Writing a full integration test from UI to database and back is one of the best ways to ensure your application meets its requirements, and also to ensure that future changes to the UI (or any other layer) don’t break existing functionality. Luckily in the web space there have been a lot of advancements in this, primarily with a framework called Selenium. Selenium abstracts away browser interaction to a common set of functionality, and then interfaces with most major browsers to execute those functions on each browser. In short, it’s a write-once, simulate-everywhere browser UI testing framework. Using this, it’s easy to test your UI in a reusable and cross-browser way.
I’m going to go on about how we use Selenium a bit, but there are a few lessons here that can be applied to other ui-based integration tests. We use the “page objects” for Selenium – basically we have a class that wraps the elements and functionality of every web page we want to test. Using this, we can create a new TemplatePage object, and have code-based access to html elements and functionality on the page. In example:
1 2 3 4 5 6 7 8 9 10 11 12 13
There’s a few nice things here, like the FindsBy attribute (automatically populates those properties based on the id of html elements), and the defined EditExistingTemplate method. The point is we’ve created a reusable object that encapsulates the UI, and any time we need to do anything with the template page we can just create one of these and automatically have it all setup. This same approach could be done with any ui-based integration tests, reducing the pain of writing integration tests for the UI and encouraging developers to add more tests. With this, our test methods look like:
1 2 3 4 5 6 7 8 9 10 11
Partial Integration Tests
Of course, you don’t “have” to simulate UI input to write an integration test. It can be valuable to test your systems from an internal start point. It’s just as easy to new up an instance of your business layer class and tell it to update the password for a user directly. After that, you can use the same classes to validate that the password has indeed changed. That’s one of the nice things about integration tests – for the most part, you can just call existing code and don’t have to worry about setting up mocks or any other test configurations.
One of the biggest pain points for integration tests is maitenance. Especially in something like a new project where the interface is very volitile, it’s easy to break existing tests. And if the tests break all the time, developers are less motivated to write and maintain them. There’s a few points to consider when thinking about the maintenance of integration tests:
- Make them easy to fix. Like the page objects shown above in Selenium, if you encourage code-reuse and make writing tests easy to do, they’ll also be easy to fix. If someone has to change 10 lines of code to re-find the checkbox that was moved to a different column, odds are no one is going to fix it.
- Code them like you’d write normal code. There’s a tendency to just “get ‘er done” when it comes to writing tests, but they should be treated with the same consideration that you’d give your application code. This includes code reuse, proper variable naming, abstracting away common functionality, refactoring, and other techniques. At the end of the day we’re still just writing code, so all the things you have access to in your normal codebase you can and should do here.
- Have a culture of testing. Our team (recently) implemented a “all tests must pass in your pull request” rule, to ensure that tests are taken seriously and not neglected. This requires that all tests in your main branch actually work, and work consistently on everyone’s machine. Get the team’s confidence in tests up, so no one can use the ‘it never worked to begin with’ excuse.
- Don’t write UI tests until the interface is solid. There’s nothing more demotivating and frustrating than having the UI tests break every week. Don’t bother with UI-based integration tests until the UI is in a state where it’s not going to drastically change every other day. Start instead with partial integration tests, and then you can refactor those and/or create new ones for the UI later.
Integration tests are hard to setup, easy to write, and even easier to break. But they’re one of the best ways to ensure that your application actually does what it needs to do in a automated and repeatable way. It’s definitely worth the time to spend a good few days setting up an integration test project with a bunch of utilities and functions designed to help other developers write tests easily. Just make sure to pay attention to them after they’re done.