If you started your Android development around the time instrumentation tests were the only tests supported out of the box, you probably remember how tedious it was to test drive an app. In fact, writing tests, whether before or after the implementation was written, was so complicated and frustrating that some of us chose to avoid them entirely.
Well times have changed and what we now have at our disposal is enough to not only write a reasonable amount of tests but also to develop our apps and libraries driven by tests. In this article I’ll share how I do TDD in Android. I won’t dive deep into the concept of TDD, I’ll simply explain the approach I use and how it has greatly improved my development. I’ll approach this with a concrete example.
A trip down memory lane
Not so long ago we – Android developers – had little or nothing more than Instrumentation Tests at our disposal to write unit tests. If you remember as well as I do, these tests were slow and practically a nightmare to maintain. The reason being that they were dependent on the Android framework and would require a device (virtual or physical) to run.
Also, while the rest of the world was already using JUnit 4 we were stuck in the past with JUnit 3. There were a couple of ports that actually made it possible to use JUnit 4, but these never saw widespread use.
Then along came Robolectric and friends to ease the pain a bit. Robolectric would mock the Android framework so that we could run our tests in the development machine rather than on a device thus eliminating the biggest reason for a slow test suite. However, it would often break with new releases and sometimes caused more issues than it fixed.
All of this contributed to the lack of a test culture within the Android world. Today, however, we are facing a different scenario. It’s been a while since Google released support for JUnit 4 and the ability to mock the Android framework. I want to share with you how I develop my apps in a TDD fashion. It should not be taken as the de facto standard. It’s also a bit behind what TDD with other frameworks looks like, where you experience very fast feedback times, but I think it deserves to be shared.
My setup
Before you read on I think it’s important to explain my setup. My experience with TDD leads me to believe that it shapes your code. It’s more like you build the code so that you can easily test it, rather than building the code and then considering how it can be tested. Therefore, I think it’s important to know and understand the design patterns and Android tools we can use before we move on.
Firstly, I always use dependency injection, and I always use constructor dependency injection except in those scenarios where it is not possible, i.e. activities. Hence why my classes usually look like this:
public class ABC {
//...
public ABC(A a, B b, C c) {
this.a = a;
this.b = b;
this.c = c;
}
//...
}
See how the constructor receives all the dependencies as arguments? This is great because it lets me pass in mocks at testing time to the constructor.
If you try this yourself perhaps you’ll see that then your class constructors will sometimes end up having several arguments. This is not unusual. This is why I use and recommend a dependency injection library that helps you out with this. I use Dagger, but there are plenty of others to choose from. You can do it even manually, there’s nothing preventing you, but using a library that does it for you will no doubt speed up the process.
Also I don’t really use anything else besides JUnit 4 with assertj.
assertj is not required at all. It’s just a library I find very useful as it gives you more fluent assertions.
I then use Mockito to help me out mocking all these dependencies during testing.
Last but not least, I use the keyboard shortcuts in the IDE. They help me develop a lot faster. I use the standard ones in Android Studio and then I just have a custom one that runs the Gradle task testDebug
. As part of this post I’ll add the shortcuts as we go, so you can also try them by yourself.
Hands on
I’d like to offer a simple hands on example for the sake of understanding, but it is nevertheless very easy to apply this approach to more complex cases.
Let’s imagine that we’re building an application which eventually will have a typical user login feature – A field for email and a field for password. I don’t want to concern myself right away with the view, so I’ll start by implementing the business logic.
For this blog post I want to take care of the validation of input fields and specifically the email field. Of course, one can argue that the validation should be done in a backend of some sorts, but for the sake of this demo I want to do some pre-validation on the client side.
I’ll first define the interface for the input field validator. My approach with TDD is to always start very simply. Here I foresee I’ll have input fields that will only contain text, so the following interface would suffice:
import android.support.annotation.Nullable;
public interface Validator {
enum ValidationResult {
/**
* The input is valid
*/
NO_ERROR
}
ValidationResult validate(@Nullable String input);
}
As you can see we have a simple method that takes a string and returns a validation result. As you might’ve noticed, the ValidationResult
only contains one enum which doesn’t cover all use cases. This is on purpose. I prefer to add them as I develop the app driven by tests, because it will tell me which errors I actually want to handle.
In TDD you start by writing the tests, but unfortunately in Java and specially with an IDE as Android Studio that’s not as easy as it sounds. The problem is that if there’s no class created yet that we can test, then the IDE will flag the tests with errors until we actually create the class itself. Therefore, I often start by creating the bare minimum implementation for each class I test drive. In this case I’d go with something like this:
import android.support.annotation.Nullable;
public class EmailValidator implements Validator {
@Override public ValidationResult validate(@Nullable String input) {
return null;
}
}
Obviously this doesn’t do much so let’s start by defining our business logic in terms of tests. The first thing I want to do is to create an empty test case with a setup method that I’ll fill in with the creation of an EmailValidator
. Try and keep your tests under the test
folder in favour of the androidTest
, because we’re not doing any instrumentation tests.
The keyboard shortcut Cmd+Shift+T
on macOS and Ctrl+Shift+T
on other OSs helps you with the test case creation. You can use the same shortcut to jump from the implementation to the test and vice-versa making your navigation way easier.
It would look something like this:
import org.junit.Before;
public class EmailValidatorTest {
private EmailValidator validator;
@Before public void setUp() throws Exception {
validator = new EmailValidator();
}
}
First things first, I want to make sure that a valid email is considered valid by my implementation. This you can express as:
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class EmailValidatorTest {
private EmailValidator validator;
@Before public void setUp() throws Exception {
validator = new EmailValidator();
}
@Test public void validate_shouldReturnNoErrorsForValidEmails() {
String input = "someemail@somedomain.com";
Validator.ValidationResult result = validator.validate(input);
assertThat(result).isEqualTo(Validator.ValidationResult.NO_ERROR);
}
}
Running the tests will result in a failure since we’re returning null
. Since I just need to make this test pass I just change the implementation to:
import android.support.annotation.Nullable;
public class EmailValidator implements Validator {
@Override public ValidationResult validate(@Nullable String input) {
return ValidationResult.NO_ERROR;
}
}
Now that the tests are passing I’ll carry on with the second bit of business logic – consider the input invalid if it’s empty and return an error code for this. Again I’ll express this in terms of a unit test:
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class EmailValidatorTest {
private EmailValidator validator;
@Before public void setUp() throws Exception {
validator = new EmailValidator();
}
@Test public void validate_shouldReturnNoErrorsForValidEmails() { ... }
@Test public void validate_shouldReturnInputFieldEmptyErrorIfInputIsEmpty() {
String input = "";
Validator.ValidationResult result = validator.validate(input);
assertThat(result).isEqualTo(Validator.ValidationResult.EMPTY_INPUT_FIELD);
}
}
The name convention for the test I’ll leave it to you. Here I use one that I like, but I understand opinions differ. At the time you create the test most probably the Validator.ValidationResult.EMPTY_INPUT_FIELD
enum constant doesn’t exist. That’s ok, go ahead and create it and run your tests.
Like I said before I have a custom shortcut to run the tests, but you can always do Cmd+Shift+A
or Ctrl+Shift+A
which will pop up a little window where you can type anything and you’d get suggestions on what you want to do. If you’re on Android studio 2 you’d want to type in something like Gradle task
hit enter and type testDebug
to run the tests. On Android 1 you can type testDebug
directly in the popup window and the IDE will automatically suggest the gradle task.
I guess it comes without surprise that the tests are failing. TDD says we cannot have any failing test so we need to either fix the code, or fix the test. There’s nothing wrong with the test and it’s pretty obvious the mistake is in the code itself. We can make it work by changing it to:
import android.support.annotation.Nullable;
public class EmailValidator implements Validator {
@Override public ValidationResult validate(@Nullable String input) {
if (input.isEmpty()) return ValidationResult.EMPTY_INPUT_FIELD;
return ValidationResult.NO_ERROR;
}
}
Now running the tests will pass with green lights. Moving on we need a failing test. As you look at the interface definition, the validate method can take null
values. Let’s try that one:
Here I also find the Cmd+E
or Ctrl+E
shortcut useful. It pops up a small window with the last open files. Usually I find myself fiddling only with the implementation and the test itself, hence this shortcut becomes pretty handy.
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class EmailValidatorTest {
private EmailValidator validator;
@Before public void setUp() throws Exception { ... }
@Test public void validate_shouldReturnNoErrorsForValidEmails() { ... }
@Test public void validate_shouldReturnInputFieldEmptyErrorIfInputIsEmpty() { ... }
@Test public void validate_shouldReturnInputFieldEmptyErrorIfInputIsNull() {
Validator.ValidationResult result = validator.validate(null);
assertThat(result).isEqualTo(Validator.ValidationResult.EMPTY_INPUT_FIELD);
}
}
As expected this fails and it’s the code that needs fixing. A simple null check is sufficient to solve the problem:
import android.support.annotation.Nullable;
public class EmailValidator implements Validator {
@Override public ValidationResult validate(@Nullable String input) {
if (input == null || input.isEmpty()) return ValidationResult.EMPTY_INPUT_FIELD;
return ValidationResult.NO_ERROR;
}
}
This kind of checking is usually done very often and developers usually use TextUtils.isEmpty()
. The problem with this approach is that you’d be making the above class dependent on a method from the Android framework, which personally I prefer not to. The reasoning is simple – the Android framework is mocked at testing time. If you want to use some of its methods you’ll most probably need to activate the setting unitTests.returnDefaultValues
in the Gradle setup. This in turn makes every method in the framework return null
, which will obviously break the tests. So usually I end up replicating some of the code on my own classes that I can easily mock the behaviour of if needed.
Moving on we can add a simple email check as well to pre-validate the email field without sending it to the backend. This step might look trivial, but it’s actually trickier than it looks. One could add android.util.Patterns.EMAIL_ADDRESS
to the implementation of the validator itself:
import android.support.annotation.Nullable;
public class EmailValidator implements Validator {
@Override public ValidationResult validate(@Nullable String input) {
if (input == null || input.isEmpty()) return ValidationResult.EMPTY_INPUT_FIELD;
if (!android.util.Patterns.EMAIL_ADDRESS.matcher(input).matches()) return ValidationResult.MALFORMED_INPUT;
return ValidationResult.NO_ERROR;
}
}
However, this makes us again dependent on the Android framework. It also makes it harder to test, since now we need to be aware of the behavior of the pattern itself, while we’re just unit testing an email validator for input fields. It also makes it harder to change the behavior later on if we find out that the pattern needs to be more or less restrictive. Here we’re just concerned with knowing that if the email is malformed, then the validator should return Validator.ValidationResult.MALFORMED_INPUT
.
Here’s where dependency injection helps me. There’s a clear dependency on how the email is considered malformed or not, so I want to isolate this. So the next logical step would be to make this pattern injectable.
import android.support.annotation.Nullable;
import java.util.regex.Pattern;
public class EmailValidator implements Validator {
private final Pattern emailPattern;
public EmailValidator(Pattern emailPattern) {
this.emailPattern = emailPattern;
}
@Override public ValidationResult validate(@Nullable String input) {
if (input == null || input.isEmpty()) return ValidationResult.EMPTY_INPUT_FIELD;
if (!emailPattern.matcher(input).matches()) return ValidationResult.MALFORMED_INPUT;
return ValidationResult.NO_ERROR;
}
}
This lets me inject a mocked pattern in the tests, which would let me specify its behaviour. While this approach is usually enough, it’s not so convenient here. If you take a look at java.util.regex.Pattern
you’ll find out that it’s final
. This makes it quite hard to mock since you’re not allowed to extend it, which also prevents you from using Mockito. You could set up patterns for each test that would either match the input or not, but again this is not very flexible. My usual approach is to define a new interface that specifies the wanted behaviour. Here I want something that returns a boolean telling me if the email is valid or not depending on its implementation – whichever that might be. Something like this:
public interface EmailRegexMatcher {
boolean matches(String input);
}
And then one can change the dependency in the EmailValidator
:
import android.support.annotation.Nullable;
public class EmailValidator implements Validator {
private final EmailRegexMatcher emailRegexMatcher;
public EmailValidator(EmailRegexMatcher emailRegexMatcher) {
this.emailRegexMatcher = emailRegexMatcher;
}
@Override public ValidationResult validate(@Nullable String input) {
if (input == null || input.isEmpty()) return ValidationResult.EMPTY_INPUT_FIELD;
if (!emailRegexMatcher.matches(input)) return ValidationResult.MALFORMED_INPUT;
return ValidationResult.NO_ERROR;
}
}
Why is this helpful? Now in the test we can create mocks to easily specify the wanted behaviour. Here’s how:
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.when;
public class EmailValidatorTest {
@Mock EmailRegexMatcher emailRegexMatcher;
private EmailValidator validator;
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(emailRegexMatcher.matches(anyString())).thenReturn(true);
validator = new EmailValidator(emailRegexMatcher);
}
@Test public void validate_shouldReturnNoErrorsForValidEmails() { ... }
@Test public void validate_shouldReturnInputFieldEmptyErrorIfInputIsEmpty() { ... }
@Test public void validate_shouldReturnInputFieldEmptyErrorIfInputIsNull() { ... }
@Test public void validate_shouldReturnInvalidEmailForInvalidEmails() {
when(emailRegexMatcher.matches(anyString())).thenReturn(false);
Validator.ValidationResult result = validator.validate("invalid email");
assertThat(result).isEqualTo(Validator.ValidationResult.MALFORMED_INPUT);
}
}
As you can see now the setUp
method sets up the emailRegexMatcher
to always return true for any given input. However, the test that wants to assess if the returned result is Validator.ValidationResult.MALFORMED_INPUT
makes sure that the email regex matcher always returns false for every input.
For the production code one can have an implementation of the EmailRegexMatcher
class that uses the android.util.Patterns.EMAIL_ADDRESS
or something else. The biggest advantage here is that the implementation of EmailRegexMatcher
can change without affecting EmailValidator
. You can even abandon email validation in the client by implementing a version of the EmailRegexMatcher
that always returns true
.
Wrap up
This is how I usually do TDD in my Android apps. It’s one of the vast possibilities out there. An important thing to notice is that with a mix of tools and software patterns the code both becomes easier to test and more decoupled.
I make use of dependency injection to easily mock the behaviour at testing time and I provide the proper implementations at run time. I only use constructor dependency injection, but one can set the dependencies in different ways. It also allows me to easily add tools like Dagger to the app and annotate the constructors with @Inject
.
I only use Robolectric when it’s absolutely necessary. Usually when I test view code that depends i.e. on the Android context.
Last but not least I took my time to learn the keyboard shortcuts and even setup some custom ones that would automate the process of writing the test, running them and jumping from the test to the implementation and vice-versa.
Photo by Angela Compagnone on Unsplash