Simple unit tests for Android

Simple unit tests for Android

The time has come to share with you this small article on testing Android app logic. It didn’t occur to me that it is a significant component of Android app development for quite a time but it’s never too late to learn, neither for you, nor for me :)

Overview

First, let’s learn what tests are and why do we need to write them. From Wikipedia:

Unit testing is a software testing method used for testing individual units of source code in order to determine whether they are fit for use.

The idea is to write code for each non-trivial function or method. It allows you to relatively quickly check if the latest change in code causes regression, i. e. new errors appear in the part of the program that was already tested, and makes it easy to identify and eliminate such errors.

Simply put, tests are methods that check the efficiency of your code. But how exactly does it all work? Let’s start from the beginning. Test is considered to be completed if no mistakes were encountered. And for various types of checks we use supplementary methods like assertXXX “family” (see examples below).

There’re 2 types of unit tests in Android:

  • Local unit tests which are tests that are performed using JVM only. They are meant for testing business logic that is not interacting with the operating system.
  • Instrumented unit tests are tests used for testing logic interconnected with Android API. They are performed on physical device/emulator and thus take more time than local tests

So we have 2 types of tests and choose the relevant one based on the goals of the logic we test. Of course, it’s better to write local tests, if it’s possible.

Also, when creating tests, pay close attention to package organization. The following structure is practical:

  • app/src/main/java — app source code.
  • app/src/test/java — local tests.
  • app/src/androidTest/java — instrumented tests.

Setup

If for some reasons you aren’t using Android Studio that generates them when you are creating a project, try to memorize the structure above. Furthermore, IDE configures Gradle file of app module:

android {
   ...
   defaultConfig {
       ...
       testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
   }
   ...
}
dependencies {
   ...
   testCompile 'junit:junit:4.12'
}

A popular framework for making test writing process easier is called Mockito. Its features include imitation of objects and overseeing them, as well as supportive tools for conducting checkups. To start using it add a dependency:

testCompile 'org.mockito:mockito-core:1.10.19'

Local tests in Android

Let’s now take a look at 2 very simple tests that check 2 objects for inconsistencies (2 numbers in our case):

public class ExampleUnitTest {
 
 
   @Test
   public void addition_correct() throws Exception {
       assertEquals(4, 2 + 2);
   }
 
 
   @Test
   public void addition_isNotCorrect() throws Exception {
       assertEquals("Numbers isn't equals!", 5, 2 + 2);
   }
}

To do it create a class in the local test package and put our methods into it. For JUnit to know that these methods are tests they are marked with @Test. assertEquals() method shows you AssertionError when the first (expected) and the second (result) do not correspond to each other.

Let’s run them (by right-clicking the class and choosing Run ‘ExampleUnitTest’ in the menu that appears) to see the output:

Test output in Android

You can see that the first method was executed and the second gets an error since 5 != (2 + 2). You can also set up an expected exception for test using an expected parameter:

@Test(expected = NullPointerException.class)
public void nullStringTest() {
   String str = null;
   assertTrue(str.isEmpty());
}

In this case the test will be executed since we expected this exception. You can specify timeout parameter for long operations and define the value in milliseconds. If the method is not executed within the defined period, the test is considered failed.

@Test(timeout = 1000)
public void requestTest() {}

There’s also a quite interesting Matchers mechanism. For example, assertThat(T actual, Matcher<? super T> matcher) accepts them as input. They are returned by methods of org.hamcrest.CoreMatchers class and belong to overlap logic operations. Let’s take a look at several of them:

assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(list, hasItem("3"));

As their names suggest, is() can be described as “is equal”, is(not()) as “isn’t equal” and hasItem() as a check for elements availability in the list. And it can be read as a coherent sentence. Here you can find a full list of matchers.

We have seen simple examples so far that can be used for writing simple tests. But I would also like you to take a look at Mockito library that can be used for making our tests superb!

As was mentioned earlier, Mockito is used for creating so called mock objects. Their goal is to replace complex objects that can’t be tested or aren’t suitable for tests. There’re two ways to declare them:

List mockedList = mock(List.class);

or

@Mock
List mockedList;

Note that in order to use @Mock annotation you should mark your class with @RunWith(MockitoJUnitRunner.class) annotation or call out MockitoAnnotations.initMocks(this); in @Before method:

@Before
public void init() {
   MockitoAnnotations.initMocks(this);
}

After receiving mocked object you can do magic! For example, in order to override app name from the string resource you can do the following:

when(mockContext.getString(R.string.app_name))
       .thenReturn("Fake name");

Now when getString() method is called “Fake string” will be returned even if it wasn’t called out implicitly:

SomeClass obj = new SomeClass(mockContext);
String result = obj.getAppName();

But what if we want to override all the string resources? It can’t be that we need to do it for all of them separately! And sure, there’re other ways, namely anyXXX(), (anyInt(), anyString(), etc. Now when you replace R.string.app_name with anyInt() (don’t forget that in Android all the resources have Integer type), all the strings will be replaced with our string.

And we can check it by writing assertThat() in the end of our test:

assertThat(result, is("Fake name"));

If we want to remove Exception, we can use when(...).thenThrow(...);.

There’s also a verify() method that will fail the test if the specified method was not called out before that. Let’s take a look at the code:

mockedList.add("Useless string");
mockedList.clear();
 
verify(mockedList).add("one");
verify(mockedList).clear();

Here’s how it works: we pass our mock list to verify(mockedList) and then specify methods we are interested in. In our case — adding string and clearing the list. An essential tool, I believe :)

But the full potential of this method opens up when you’re using spy objects. The main difference is that, unlike mocks, they aren’t created directly. You might ask: “So what’s the point then?” The point is that by creating a “spy” you can oversee it like a fake object:

List spyList = spy(new LinkedList());
 
 
spyList.add("one");
spyList.clear();
 
 
verify(spyList).add("one");
verify(spyList).clear();

Instrumented tests in Android

Using this type of tests we get access to real context, as well as all the Android API capabilities. Apart from it, there’s a “God mode” we can use for managing the activity life cycle. For test to be recognized as instrumented, you should mark the class with the respective annotation:

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
   ...
}

Tests themselves are marked in same way as local tests — with @Test annotation. Let’s check whether the package of our context corresponds to our app:

@Test
public void useAppContext() throws Exception {
   Context appContext = InstrumentationRegistry.getTargetContext();
   assertEquals("com.stfalcon.demoapp", appContext.getPackageName());
}

Using the available tools you can listen to activities’ status:

ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(new ActivityLifecycleCallback() {
   @Override
   public void onActivityLifecycleChanged(Activity activity, Stage stage) {
       //do some stuff
   }
});

...or even manage their status:

Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
instrumentation.callActivityOnCreate(activity, bundle);

Conclusion

These are probably the most important things I found when familiarizing myself with the topic. Don’t throw stones at me :) But I think that it would be enough for starters. If you want to learn more about junit capabilities, I recommend you to read official documentation and study info on capabilities of the instrumented test.