Writing automated tests is a tried and tested way to improve the quality of software. In the initial phase of development, tests help to verify that the code functions correctly. In Test Driven Development, tests are written before the code, so any knowledge about the ins and outs of the implementation won't influence the writing of the tests. After the initial development, as changes are made over time, a comprehensive test suite can quickly point out unintended changes in the behavior of the code, so the bugs can be fixed before the software is shipped. Importantly, the tests are automated, so they are very cheap to run in terms of time.
What do we want from a test suite?
There are a range of properties that are good to have in a test suite.
- It often allows bugs to be pinpointed to a relatively small area of the code base, saving debugging time.
- It should run quickly enough to make running the suite feel like something developers can afford to do regularly. I've worked on projects where the test suite could take a very long time to run, and the consequence was inevitable: people didn't always run the tests before committing changes. I'm at least as guilty as anyone else on the project for doing this, especially when hacking on my slow old laptop.
- It tests not only individual bits of the software, but also tests them running in combination.
This last point is important. We want to be able to test relatively small units of code in isolation, giving us the ability to pinpoint the problems. However, we also need integration tests. Sure, two classes might work fine on their own and pass their tests, but what happens when you start using them in combination?
The tests for small units of the code are easy enough to write for some things. For example, the tests for the data access layer may not depend on any other bits of the code. However, the class that uses the data access layer to pull data from the database and do a series of complex calculations based on it is harder to test in isolation.
Testing it in combination with the data access layer doesn't tell us right away where the bug lies if a wrong result comes out: in the data access layer, the data in the database or in the calculation code. Moreover, database queries take time, and slow down the running of our tests. And if we want to test the calculations with some edge cases, we have to go and add them to the database, or write code to do so. This is a simple example, but as you build complex software with complex dependencies, it gets more and more important to try and test individual pieces of the software both in isolation and rapidly.
Introducing Mock Objects
A mock object is one that has the same interface (that is, the same properties and methods) as some real object, but unlike the real object just returns dummy data. It will also validate the data that we pass it as arguments, simulating failure when these arguments would have led to a failure.
When we run tests for a class that would normally use the real object, we can instead supply it with the mock object. This allows us a new level of flexibility in our testing.
- We can modify the mock object during the tests to get it to get it to return a range of different data - valid, invalid, edge cases and so forth - to test how the code calling it handles such situations.
- We can have the mock object simulate failures, such as an inability to connect to a database, to test the failure mode of the code using in.
- We can log exactly what the code using the mock object did (e.g. what methods it called, and with what parameters), which may aid the debugging process.
Additionally, because the mock object is not really doing a great deal of work, but instead is just returning data, our tests may be able to run much more quickly.
Imagine we made a mock object for our data access layer. Since it didn't really have to go off to the database to fetch the data or do the updates, the tests would be a lot faster to run. Additionally, we could return a range of different data and test how the code doing the calculations responds without having to actually modify the database itself. We can also simulate database failure without taking down the database!
Mocking Your Language
Writing your own classes for mock objects would be tedious. Thankfully, a range of mocking frameworks exist, which help take the boring work out of creating mock objects for you by allowing you to specify the mock objects more declaratively. In a future article, I'll take a look at NMock, a mocking framework for .Net. In the meantime, here are some mock object frameworks for different platforms and languages.
Happy mocking!