Android Unit Testing with JUnit5

Jul 27 · 6 min read

In Android, almost all our unit tests are written using JUnit4, which came with Android Studio. One huge problem we faced in the readability of our test cases. A typical project will easily contain 1000+ unit test cases.

Naming these 1000+ test functions became an issue. Whenever a test case fails, it can take a significant amount of brainpower just to figure out what exactly the test case was doing.

With JUnit5, it allows your unit tests to be well-documented. This is important because nobody will want to read badly documented stuff.

This article has two sections.

The first part, we will be sharing some of the methods used to name test cases and discuss the motivation for using JUnit5.

The second part shows how we write JUnit5 tests for Android.

An Evolution in Naming

In the beginning, we name our test cases like this:

@Test
fun testWhenButtonClickedThenIfFlagTrueLaunchActivity(){...}

While if you try hard enough, you could figure it out. But it was difficult for any reviewer to see if your test class actually cover all cases. We have only one flag to check here, what if we have another condition? When you have hundreds of test cases to go through, your brain fries.

Then we discovered Kotlin allows us to do this:

@Test
fun `When button is clicked, If flag is true, then launch activity`() { ... }

It was a little eureka moment for us. Along with this, we discovered many have been trying to name test in a behavior-driven-development way. So we did that too. We place conditions in Given, triggers as When and expected outcomes in Then.

@Test
fun `Given flag is true, When button is clicked, Then activity is launched`(){ ... }
@Test
fun `Given flag is false, When button is clicked, Then activity is not launched`(){ ... }

This works better, but it was still difficult to read, especially if we have multiple conditions.

@Test
fun `Given flag is true and listener null and user is logged out, When button is clicked, Then activity is launched and listener not called`(){ ... }
@Test
fun `Given flag is true and listener not null and user is logged out, When button is clicked, Then activity is launched and listener not called`(){ ... }

Were you able to tell that that test above is trying to say that the listener will not be called so long as the user is logged out?

Running such tests in Android Studio will give you an output like this.

Test results in Android Studio

You have to invest a significant amount of time to comprehend this. When you have hundreds of test cases, your test suite simply became a black box in which you send your code through. Like sending a hero through a dungeon hoping to see him on the other side. While the entire test suite was helpful, it was a huge maze. While you could figure out what caused the failure, it took an arm or leg.

Better Documented Test Results Using JUnit5

This might be the best thing that happened to Android teams writing tests. There are many new features introduced in JUnit5, but two powerful features stood out:

  • Nested Classes
  • Display Name

Let’s take an example where we want to change a state to logged in or out depending on whether a session data is valid or not. Below is an example of what is possible when we moved from JUnit4 to JUnit5.

Test results using JUnit4
Using JUnit5‘s Nested classes and DisplayName annotations

This really shines when you have multiple asserts in your test. For example, you want to check state is logged out and a listener is called.

So some may argue we should not have multiple assertions in test cases to keep them “unit”. Okay, so we shall separate the assertion,

Again, better visibility of what get’s check using JUnit5.

You can judge for yourself which format you preferred. Especially when a test fails.

Let’s see how to write tests using JUnit5 in Android.


Implementing JUnit5

There is a great article explaining some concepts of JUnit5 and how to configure them by Andrew at his blog. Here are just some quick “get-it-to-work” stuff.

Configuring Gradle

We used the Gradle plugin provided by Marcel Schnelle (mannodermaus).

Add the plugin to our root build.gradle file.

buildscript {
dependencies {
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.3.2.0"
}
}

Then for app/build.gradle apply plugin,

apply plugin: 'de.mannodermaus.android-junit5'

And then import the Gradle dependencies for JUnit5,

dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.2"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.2"
testImplementation "org.junit.jupiter:junit-jupiter-params:5.3.2"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:5.3.2"
}

These setups will allow your project to run both JUnit4 and JUnit5 test cases.

Here is an empty JUnit5 test class, which will look familiar with setup tear down and one test function.

For the @BeforeEach here, we can do our mock setups or test class init. This function will be called before each test function, be it nested or not nested.

We used the given, when, then convention. For our example, we have a condition to check that sessionData is valid or not. So we create two nested classes,

Each of these nested class can also have their own setup and teardown code. So let’s add different setup code for each inner class. One returns true when userSession.isValid() is called and the other, false.

Next, we have another nested class to trigger the test — our when.

Next, our then conditions, where we verify or assert stuff. We add them as test functions.

So running this test will give us the following output,

We can also choose to do this and produce a less nested structure.

Doing this helps in designing test cases, as you can quickly see that I am probably missing a verification,

When onDestroy(), then flag change to ?

for the case where SessionData is valid.

Some guidelines, when there are a lot of verifications or assertions, it is probably better to nest them so as to group them. It is easy on the brain as to how many things are done under a certain trigger.

Using nested class is not just for better output. You should also consider grouping similar assertions in @AfterEach under a nested class, or test cases with similar setup should be in a nested class as well.

For example, if 5 tests have the same setup code, consider placing them in a nested class, under @BeforeEach, and give them proper naming.

Conclusion

This is just a small part of what JUnit5 can do. It has many other functions not mentioned here and it is worth a look. Do start to design better test classes to make life easier for other developers.

Boon Keat Teo

Written by

Android Developer, SPHTech

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade