This is the first of a series of posts aimed at showing and discussing best practices on TDD and Android. The title is obviously an homage to the GOOSGBT book, as it shares the same purpose of showing how a real application grows guided by tests.
The project I'm working on is a Github Android client, and I’m trying to make every commit as self-descriptive as possible, so that you can follow a logical series of action jumping from commit to commit. These blog posts are a way to expand those commit messages, giving more insight on why certain choices are made, discussing alternative approaches and gathering feedback from other developers. The next parts will cover how to build a walking skeleton and mocking the API service.
While GOOSGBT is still a great reference for TDD and Java, Android developers face particular challenges while testing their code, mainly because the lifecycle of key objects like Activities and Fragments is managed by the framework. Thankfully, Android testing has gone a long way since its inception, and now we have an interesting array of tools tat our disposal. We can use Robotium to drive our integration tests and Robolectric to unit-test objects that depend on Android OS. We can use Injection to mock singletons and inject dependencies on the Activities and REST clients like Retrofit to abstract over a third-party service.
Of course, this application is mainly a pet project (see Breakable Toy), where I can try out new ideas and verify alternative strategies that are hard to setup in production code. Sometimes you'll find things that will make a TDD purist cry, sometimes I'll skip a few steps just to include an interesting library. That’s ok, if you see something that you don't like you're welcome to make a pull request.
But enough for the introduction, let's jump right into the code. Every paragraph will show the link to the commit I’m discussing, so that you can read the post and the code side by side.
Define the project structure as follows:
github-androidcontains the actual Android Application and the unit tests for its declared objects. It depends on github-android-core.
github-android-corecontains plain java objects used to exchange information with external services (e.g. an api server) plus the associated unit tests.
github-android-itcontains the integration tests used to verify the overall behaviour of the application. It depends on github-android.
github-android-smokecontains test fixtures used to verify the expected behaviour of external services. It depends on github-android-core only, so that the fixtures can run on a plain JVM.
We start with a project configuration that is an extension of the plain maven release archetype for android. What is crucial in this step, and I can't stress it enough, is that you want, you really want to mock the external services that your app uses.
Just as in GOOSBT, you may build your application against a service that you can't access, and you have to rely on its documentation. But more often, you find yourself in a situation where the external service is half-completed, with some features already implemented, some missing, and some that will change in the future. Having a dummy service at hand is a lifesaver in these situations, because you can quickly swap the original service for your mock, test and implement the required features, and merge them back to production when the apis are stable. The worst-case scenario is when the service is being implemented at the same time as your application: in this case not having a dummy service abstraction is just like digging your own grave.
But even if you're working against a reliable service (as we are, with our Github application), I think you should go the extra mile and use this configuration anyway. This is because it has several advantages:
It makes writing tests easier
Knowing the response in advance is a huge advantage because you can predict what you should look for in your tests.
It makes running tests easier
No network activity means faster and reliable tests. If a build fails you know that it's your fault. If your build starts failing because of random connection timeouts, server outage, or something else you can't control, you tend to ignore your “Build Failed” messages, and your CI become useless.
It pushes you to write more flexible code
Your core package is the bridge between the service and your application. Here is where you validate your models, where you abstract over the service interface and where you try not to let external concepts leak in your application. This is your first line of defence against external changes, and in a perfect world, if something changes in the external services implementation you would only need to touch you core package, making it transparent for the rest of the application. As a minor plus, being a pure Java project means that this module is potentially reusable in other applications (e.g. Desktop or J2EE) that interact with the same service.
It pushes you to write tests for the service you're mocking
If you mock an existing service, you want to be sure that your predictions are correct. In order to ensure that, for every expectation you have you write a test. This is a good way to understand the service and it's code that you want to write anyway because it is necessary in order to place the correct calls. Moreover, you can catch unexpected or wrong behaviour early on, before it crashes your application and you have to do an intense debug session just to discover that it's not your fault. You will find those test useful even if you just want to double check a particular call: it is really handy to have a named test method for that, instead of copying and pasting urls in your browser, or using external scripts.
You should have some cached responses for testing anyway
When you're writing your POJO deserializers you need a proof that the deserialization is always successful, right?
It is always nice to run your app in mock mode:
The service is down for the rest of the day. Or you have to show the app to the client and want to avoid random crashes. Or you need to verify the behaviour in a particular edge case (a network timeout, a particular response, a user with too many objects in its basket etc.). Or the service is rolling api v2 today breaking retro compatibility with your application. Or you're stuck in some weird place with no network connectivity.
Bottom line: once you have a mock mode ready at your disposal, you’ll find more and more uses for it.