Write Your First Unit Test in Android Using JUnit4 and Truth Assertion Library

Clint Paul
Jan 16 · 10 min read
Image for post
Image for post
Photo by Startup Stock Photos from Pexels

Let’s be honest. Writing test cases in software development is either religiously followed or very much overlooked. There is no middle ground in it. But, whatever the case, it is an essential skill needed for a software developer. You will need to write test cases. If not today, then tomorrow. I’ll give a small introduction about the TDD architecture and testing in general before we start working on an example.

You must have heard the term TDD a lot in recent times. TDD stands for test-driven development. As the name suggests, you will develop your software based on writing test cases.

Image for post
Image for post
fundamentals of testing

Imagine you are working on a new feature for your app. Firstly, we will give it a thought before working on it to guarantee that we are not missing any edge cases. The problem with that approach is that we can overlook a few edge cases, and we will be able to run the code only in a running app. As more and more features get added, testing will be difficult and more time consuming than ever. The TDD approach is an excellent strategy to solve this issue.

As the image suggests, you will write the test case first before you write the actual function. If you are not familiar with this approach, it will be very confusing and incomprehensible. Imagine you write a test case for a method that doesn’t exist. That is the beauty of the TDD architecture. Each of these methods is called units. Writing test cases and testing those units are called unit testing. Your unit test must include all the possible interactions with the unit, including the standard interactions, invalid inputs, and cases where resources aren’t available.

Image for post
Image for post
Testing pyramid

The testing pyramid will classify the different types of tests according to their importance and granularity. In Android, there are three types of tests.

  1. UI tests ( Large tests ) ( Fidelity is excellent, larger execution time, hard to maintain )
  2. Integration tests ( Medium tests ) ( Fidelity is good, medium execution time, less difficult to maintain )
  3. Unit tests ( Small tests ) ( Fidelity is low, faster execution time, Easy to maintain )

As you can understand, unit tests are easy to implement, and UI tests and integration tests will take more time and maintenance. Generally, we will split the categories like the following: 70 percent small, 20 percent medium, and 10 percent large.

Android unit testing

Unit tests are the fundamental tests in your app testing strategy. By creating and running unit tests against your code, you can easily verify that the logic of individual units is correct. Running unit tests after every build helps you to quickly catch and fix software regressions introduced by code changes to your app.

For testing android apps, there are two types of automated test units available.

Local tests: The reason they are known as local tests is that they will be running on your local machine. In our case, our laptop or PC. They run on the local JVM. They don’t require either an emulator or physical android device to run. They are faster in execution, but provides less fidelity.

Instrumented tests: They will run only on physical or emulator devices. Instrumented tests provide more fidelity than local tests, but it is slower in execution. Therefore, it is recommended to do an instrumented test, only if you must test against the behavior of a real device.

I think now is an excellent time to take a deep breath and read everything one more time. Drink some water or coffee and come back. We are going to create a new project and then run our first unit test.

Create a new project and go to the app’s build gradle file.

//    Testing
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

The three dependencies mentioned above will be already added to your project by default. You can see that there are two types of dependencies added. ‘testImplementation’ and ‘androidTestImplementation’. The dependencies related to local tests will come under testImplementation and those that related to instrumentation tests will come under androidTestImplementation. Let’s now examine these dependencies.

testImplementation 'junit:junit:4.13.1': JUnit is the most popular and widely used testing framework for java. Integrating this dependency will allow us to write test-cases more cleaner and flexibly.

androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0': The espresso testing framework is an instrumentation based API and will help us to test user interactions to avoid unexpected results. Since we are only talking about local unit tests today, we don’t have to worry about espresso at the moment.

The basic principle of testing is that, we have to assert if the actual result and the expected result are either true or false. In order to help us to check this scenario, which is called an “assertion”, we have a simple and beautiful library called, truth from Google. JUnit can also do assertions. But, truth can do it much better. I felt like it is much easier to read and has less boilerplate code. Let’s integrate truth dependency now.

testImplementation "com.google.truth:truth:1.1"

Your build gradle ( app ) will look like this now,

plugins {
id 'com.android.application'
id 'kotlin-android'
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.2"

defaultConfig {
applicationId "com.clintpauldev.unittestingsample"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

// Testing
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

// Assertion
testImplementation "com.google.truth:truth:1.1"
}

We are going to create a test-case for a function which validates registration inputs such as username, password and confirm password. We will try to include as many as possible edge cases. Such as,

  1. The input is not valid, if the username or password is empty.
  2. The username is already taken.
  3. The confirmed password is not same as the real password.
  4. The password contains less than two digits ( Optional ).

Create a new object class called RegistrationUtil and add a new function inside it, validateRegistrationInput() which contains three parameters and return a boolean. The parameters are user name, password and confirm password. Since, we don’t have a running dB, I’m using a static list of data for this example.

object RegistrationUtil {

private val existingUsers = listOf("Peter", "Mathew")

/**
* the input is not valid if
* the username/password is empty
* the username is already taken
* the confirmed password is not the same as real password
* the password contains less than 2 digits
* */

fun validateRegistrationInput(
userName: String,
password: String,
confirmPassword: String
) : Boolean {
return true
}

}

Now, don’t think about adding the logic to the function. Instead, let us create a new test class for this function.

  • Right click on the object name RegistrationUtil and click on Generate.
  • Click on the test button
Image for post
Image for post
How to generate a test class
  • Make sure to select testing library as JUnit 4 and click on OK.
Image for post
Image for post
  • Choose the destination directory as ‘test’ and click OK.
Image for post
Image for post

Congrats. You have created a new testing class for your RegistrationUtil class.

Now, let us write the test case for the following condition.

  • The input is not valid, if the username or password is empty.
@Test
fun `empty username returns false`() {
}

Create a new function called empty username returns false inside the RegistrationUtil class. You might have noticed that a backtick ( ` ) is added before and after the function name. The compiler will convert the space inside the backtick into underscores. It will help in the readability of the test class. Also, notice that we have added an annotation called @Test before the function name. This annotation will tell the JUnit that it must run as a test case.

Next, we should create a new object of the validateRegistrationInput() and pass the username as empty. This test case must return a false value since the username is empty.

@Test
fun `empty username returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
userName = "",
password = "123",
confirmPassword = "123"
)}

Remember what I said earlier about the assertion? Now, we will have to assert if the result we got is true or false . We can use the help of Google truth library for that.

@Test
fun `empty username returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
userName = "",
password = "123",
confirmPassword = "123"
)
assertThat(result).isFalse()}

Let’s assert that the result is false indeed. Make sure you are importing the correct dependency for the assertion.

import com.google.common.truth.Truth.assertThat

Now, click on the play button on the left side of the function name.

Image for post
Image for post
Run the test

The test will obviously fail. Since, we are returning a true value by default in the validateRegistrationInput() function. But it is good practice to check the details of error message showing in the android studio. It will help us to know which test case failed, what was the reason, additional description etc.

It says the error occurred inside the RegistrationUtilTest class. Inside that, the empty username returns false function failed.

Image for post
Image for post
Error occurred in RegistrationUtilTest class

Also, additional error description showing that the test case expected the result to be false instead the result was true .

Image for post
Image for post

Now take a few minutes time to read and understand what we have discussed so far. After that, try to write the test-cases for the remaining conditions and run it. I’ll anyways add the remaining test cases here for you to crosscheck. But it is always better if you are trying it yourself and check how much you have learned.

  • Valid username and correctly repeated password returns true
@Test
fun `valid username and correctly repeated password returns true`() {
val result = RegistrationUtil.validateRegistrationInput(
"clint",
"123",
"123"
)
assertThat(result).isTrue()
}
  • Username already exists returns false
@Test
fun `username already exists returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
"Peter",
"123",
"123"
)
assertThat(result).isFalse()
}
  • Empty password returns false
@Test
fun `empty password returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
"Peter",
"",
""
)
assertThat(result).isFalse()
}
  • Password repeated incorrectly returns false
@Test
fun `password repeated incorrectly returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
"Peter",
"123",
"1234"
)
assertThat(result).isFalse()
}
  • Password contains less than 2 digits returns false
@Test
fun `password contains less than 2 digits returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
"Peter",
"1abcd",
"1abcd"
)
assertThat(result).isFalse()
}

I hope you were able to write all the above test cases. Now, run all the tests at once by clicking on the play button on left of the class name. As expected, out of the 6 test cases, only 1 will be passed. Now, let’s move to the validateRegistrationInput() function and write the logic.

fun validateRegistrationInput(
userName: String,
password: String,
confirmPassword: String
): Boolean {
if (userName.isEmpty() || password.isEmpty()) {
return false
}
if (userName in existingUsers) {
return false
}
if (password != confirmPassword) {
return false
}
if (password.count { it.isDigit() } < 2) {
return false
}

return true
}

I think the above function is self explanatory as it is satisfying all the test-cases we have written so far. Now, go to the test class again and press the play button. All the test cases will be passed now.

That’s the feeling. I know

Your final RegistrationUtilTest class will look like this

package com.clintpauldev.unittestingsample

import com.google.common.truth.Truth.assertThat
import org.junit.Test

class RegistrationUtilTest {

@Test
fun `empty username returns false`() {

val result = RegistrationUtil.validateRegistrationInput(
userName = "",
password = "123",
confirmPassword = "123"
)
assertThat(result).isFalse()

}


@Test
fun `valid username and correctly repeated password returns true`() {
val result = RegistrationUtil.validateRegistrationInput(
"clint",
"123",
"123"
)
assertThat(result).isTrue()
}


@Test
fun `username already exists returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
"Peter",
"123",
"123"
)
assertThat(result).isFalse()
}

@Test
fun `empty password returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
"Peter",
"",
""
)
assertThat(result).isFalse()
}

@Test
fun `password repeated incorrectly returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
"Peter",
"123",
"1234"
)
assertThat(result).isFalse()
}

@Test
fun `password contains less than 2 digits returns false`() {
val result = RegistrationUtil.validateRegistrationInput(
"Peter",
"1abcd",
"1abcd"
)
assertThat(result).isFalse()
}


}

Watching all your test cases showing green tick is an amazing feeling. I hope you were able to get the correct results as well. If you faced any problems, don’t worry, I have shared the completed project here. I have learned more things about testing after watching this tutorial by Philipp Lackner. If you are interested in learning more about testing do watch it.

I hope you were able to learn the basics of unit testing and a brief idea about TDD and testing in general. Happy coding. Stay safe.

:)

Article was originally posted at clintpauldev.com.

The Startup

Medium's largest active publication, followed by +771K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store