Android unit tests explained
Have you ever tried unit testing in Android? Haven’t you ended up so frustrated that you thought it wasn’t worth it? In this article we’re going to go through 3 simple examples that might help you get started and discover some neat tricks.
In Android there are two types of unit tests:
- Instrumented: they are slow, only run on a real device or an emulator and live in the app/src/androidTest folder by default
- Local or non-instrumented: they are very fast, run on your computer’s JVM and live in the app/src/test folder by default
Non-instrumented tests are always preferred, but they are not always possible: you can’t unit test code in your JVM that uses classes provided by the Android framework.
This means that you cannot use classes from the the android.* or org.json.* packages in your unit tests if you want to run them on the JVM. You can still unit test these classes on your JVM with Roboelectric (which is a great tool), but I didn’t want to use it here for the sake of simplicity.
Only if you are careful enough while programming you will be able to separate your code from the framework’s code. Read about clean code and hide/wrap implementation details. And please, pull your business logic out of activities/fragments to make it more testable!
In this article, we are going to see how to test these 3 examples:
- JsonParserTest: instrumented and non-instrumented
- FileSerializerTest: non-instrumented :)
- SharedPreferencesTest: instrumented
This project is in Github. In most of these tests we will use a very simple Item object that is basically:
public class Item implements Serializable {
private String key;
private boolean value;
... contructor, getters, setters, equals ...}
1. JsonParserTest
public class JsonParserTest {
@Test
public void serialize_deserialize() throws Exception {
JsonParser jsonParser = new JsonParser();
Item item = new Item("myKey", true);
String s = jsonParser.serialize(item);
Item item2 = jsonParser.deserialize(s);
// Verify that the received data is correct.
assertEquals(item, item2);
}
}
JsonParser just serializes an Item object to a String and then deserializes it back to an Item object. JsonParser uses classes from the org.json.* package that are provided by the Android framework, so this is an instrumented unit test and needs to run on a device or emulator.
There is a way of testing the JsonParser object on the JVM: adding the json classes to your gradle dependencies (only for testing purposes), so these classes will be used instead of the Android framework ones.
testCompile ‘org.json:json:20140107’
Now, if you move your JsonParserTest class from ‘androidTest’ to the ‘test’ folder, it will work. You just made it much faster! You can see it working here.
2. FileSerializerTest
File operations in Android usually need a context so that the path is relative to your app. But in this case we are going to use a trick to make it run on the JVM! (the java.io.* package comes from the JDK, not the Android framework). The code for the FileSerializer is here.
public class FileSerializerTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void save_and_load_file() throws Exception {
FileSerializer serializer = new FileSerializer();
File myFile = folder.newFile("myfile.txt");
Item item1 = new Item("myKey", true);
serializer.save(myFile, item1);
Item item2 = serializer.load(myFile);
// Verify that the received data is correct.
assertEquals(item1, item2);
}
}
As you can see, we are using the JUnit rule TemporaryFolder that gets the Android’s context.getCacheDir() for us. This folder will be implicitly created and destroyed for every test (in the @Before and @After methods).
If you needed another relative folder you might need a context, and the test will become instrumented. Next example (shared preferences) uses a context.
3. SharedPreferencesTest
This is undoubtedly an instrumented test, since it needs Android’s classes. So I’m going to show you how to get hold of the Android context for instrumented unit tests.
public class SharedPreferencesTest {
private static final String PREFS_NAME = "prefs";
private static final String KEY_PREF = "KEY_PREF";
private SharedPreferences sharedPreferences;
@Before
public void before() {
Context context = InstrumentationRegistry.getTargetContext();
sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
@Test
public void put_and_get_preference() throws Exception {
String string1 = "test";
sharedPreferences.edit().putString(KEY_PREF, string1).apply();
String string2 = sharedPreferences.getString(KEY_PREF, "");
// Verify that the received data is correct.
assertEquals(string1, string2);
}
@After
public void after() {
sharedPreferences.edit().putString(KEY_PREF, null).apply();
}
}
The the important line is:
Context context = InstrumentationRegistry.getTargetContext();
It will retrieve your application’s context. There is another method in the InstrumentationRegistry class called getContext(). Be careful and don’t use this one since it will retrieve the test application’s context.
Useful official documentation:
Part 2 is about reading assets. This project is in Github. There are instructions in the README file. Happy unit testing!