Approaching TDD Outside-in on Android (Pt. 1)

An Outside-in Test-Driven Development (TDD) can be a challenge to implement. In this 3-part post series, we would like to share our experiences applying it to Android development and offer some practical tips for doing so yourself.

In this first post of the series we will introduce the necessary concepts and present our broad approach to the problem. In most of cases, we tend to know the general components that compose a feature's design upfront. For this reason, outside-in TDD suits our process best as it allows you to develop faster than the 'baby steps' approach followed in the inside-out approach.

In order to show how we adapted outside-in TDD to Android, and to make it easier to follow along, we'll illustrate the process using the "Bank Kata" that Sandro Mancuso uses in his "Outside-in TDD" screencast. (You don't have to watched this screencast to follow along but it's useful as a primer on the concepts.)
In his screencast the problem isn't built specifically for Android, but we found it useful to have it as a focus so we decided to re-use it.

The problem description is as follows:

Create a simple bank application with the following features:

• Deposit into Account
• Withdraw from Account
• Show a bank statement

The original kata provides a class with the following structure:

public class Account {
public void deposit(int amount) {
}

public void withdraw(int amount) {
}

public void showStatement() {
}
}
And one constraint: You are not allowed to add any other public method to this class.

But, we broke that rule

It's worth mentioning that we broke this constraint somewhat to add another public method to the Account class. We did this to in order to attach the view to the account object. We needed to do so because we’ve chosen to use Android activities, and as we all know and suffer, they are instantiated by the system. Therefore, we can not pass the view through the BankAccount class constructor.1

Extracting the acceptance criteria

Let's start by describing the best-case workflow for tackling this problem. The starting point should ideally be a user story defining what needs to be done, who are we building it for, and why we are building it.2 The starting point should be a user story defining: What needs to be done, who are we building it for and why we are building it. The Bank Kata problem description above talks about three features so there should be three user stories. For the purpose of this post, we are going to focus on the show statement feature. The user story for the show statement could be written as follows:
Story: Show account statement
As a user
I want to be able to show a transactions details statement
So that I can easily check my account balance at any given time


With the user story completed, we can now define the acceptance criteria - the series of results that required in order for the feature to be considered done.

Once we have the user story, the next step would be to extract the conditions that the software must satisfy to be accepted, known as the acceptance criteria. Those acceptance criteria will define a series of results that must be validated to consider that the feature is done.

The acceptance criteria that we have came up with for the “Show account statement” story are:

Scenario 1: Account with transactions

Given the account has the following transactions:
- A deposit of 1000 on 01/04/2014
- A withdraw of 100 on 02/04/2014
- A deposit of 500 on 10/04/2014
When the user shows the account statement
Then the statement should be a list with all the transactions in reverse chronological order
And the statement lines should contain the transaction amount, date and running balance


Setting up the project

Before getting our hands dirty with the code, we have to configure the tools that we are going to use to write our tests. We have chosen JUnit, Mockito and Espresso for assertions in Android views. We have to add their dependencies to the project build.gradle as follows:

testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.17'

androidTestCompile "com.android.support.test:runner:0.4.1"
androidTestCompile "com.android.support.test:rules:0.4.1"
androidTestCompile "com.android.support.test.espresso:espresso-core:2.2.2"
androidTestCompile("com.android.support.test.espresso:espresso-contrib:2.2.2") {
exclude module: 'recyclerview-v7'
exclude module: 'support-v4'
}
androidTestCompile 'org.mockito:mockito-core:1.10.17'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'

And at the bottom of the file:

resolutionStrategy.force "com.android.support:support-annotations:\$supportLibraryVersion"

This last line is needed due to the different versions of the support-annotations that are bundled with the espresso libraries and the one that you probably already had in your project.

For the sake of clarity of this exercise, we are going to use the default source set src/androidTest for our acceptance tests and src/test for the unit tests. This configuration may not be the ideal in a production environment, due to the fact that you would need to add some unit tests for the Android components in your project. If you are in that case, you could end up with a mix of unit/acceptance tests in the same directory, losing the possibility to run all the tests quickly. Remember that acceptance and integration tests are slower than unit tests.

In a real project we will need to run all the unit tests alone to ensure that they are passing and checking the current step in the inner TDD loop. That is why is a good idea to separate them.

Writing an acceptance test for the acceptance criteria

This image show the testing flow that we are going to follow: the double loop of TDD. The outside loop corresponds to the progress of our feature and the inner loop corresponds to the individual functionals required to implement the feature.

It's worth defining our test types very clearly.

• Unit test: Test that our class does the correct thing
• Acceptance test: Test that our system passes the acceptance criteria and therefore behaves properly using real collaborators. Leaving aside external systems such as network, database, API, etc…
• Integration test: Tests that our system works together with external dependencies.

So, now that we have our requisites, we have to start writing an acceptance test. It will provide the current step of the outside loop we are in. We can re-run this test anytime to figure out what the progress of our feature is.

This test validates that our system complies with the acceptance criteria that the business owner has agreed to, being the bridge between developers and business. Once the feature is finished, the acceptance test will also serve as a regression test, quickly altering us if future code change breaks or changes the functionality. For now, it is going to offer us feedback about the progress. The acceptance test should be as end-to-end as possible, but still within the boundaries of your system and not relying on any external systems or dependencies.

The naming conventions that we are going to use come from this Codurance article. The unit tests of our classes are going to use the suffix “Should” which allows us to read the class name and the test method name as a full sentence. For the acceptance test we are going to use the suffix “Feature” so we can easily differentiate between acceptance and unit tests.

For the test implementation of this kata we are going to use the notation Given, when, then. This will allows to express the acceptance criteria in the code as closely as possible within the code itself. It will help us to use the ubiquitous language that the business owner has used to write the acceptance criteria. It is a win-win - we have the test written conforming to the 3-As pattern and we gain a ubiquitous language from the business.

To assert our views in Android (i.e check the 'then' phase) we are going to use Espresso, which provides a simple way to test the state of the UI.

But that is it for now! So far we have reviewed the basic concepts about TDD and, more specifically, the outside-in approach. In the next post we will start creating our first acceptance test and we will dive into the inner TDD loop until we get our feature working. Stay tuned!