Introduction to Unit Testing In Android (Part One): Understanding the basics
“If you are not writing test you are writing an instant legacy code”
Those words came from one of the most brilliant Software Engineer I’ve had the privilege to work with. As engineers, testing isn’t optional — it’s essential.
Today, we’re diving into unit testing in Android — whether you’re writing your first test or looking to level up your testing skills. Let’s get started!
What are unit tests in Android?
Unit testing refers to testing individual “units” of code, usually functions or methods, in isolation from the rest of the application. this ensures your code/function works as expected on its own and that it passes certain conditions independently.
Think of unit testing as checking each small part of your program, like individual building blocks, to make sure they work exactly as intended. Instead of testing the whole house, you’re checking each brick, window, and door separately. Specifically, you’re testing individual pieces of code, usually small functions, to see if they produce the right results when you give them certain inputs
Why is Unit Testing Important?
- Detects Bugs Early: It lets you catch mistakes early before they cause bigger problems later. Imagine finding a cracked brick before the whole wall is built.
- Confidence In Code Reliability: It gives you confidence that each piece of your program is reliable. If each part works, the whole program is more likely to work under any environment
- Easy to Refactor: It makes it easier to change or update your code in the future. If you know each piece works on its own, you can make changes without worrying about breaking everything else.
- Concrete documentation: It acts as a form of documentation, showing exactly how each function should behave.
Setting up your project for unit tests
To start unit testing in your Android project, you’ll need to add some dependencies to your build.gradle
file. Here’s a list of the key libraries that are commonly used:
dependencies {
// JUnit for unit testing
testImplementation 'junit:junit:4.13.2'
// Coroutine testing library
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
// Mockito for mocking objects
testImplementation 'org.mockito:mockito-core:5.5.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.1.0' // Kotlin-friendly version
// MockK for Kotlin-specific mocking
testImplementation "io.mockk:mockk:1.13.8"
}
So, where do we actually write these tests? In Android, you’ll find two key places, depending on the type of testing you’re doing:

- Unit Test: These are the smallest and fastest tests that focus on testing a single component or function in isolation. They are meant to verify that individual parts of your code (like methods or classes) work as expected. They are written inside the
test
directory - Integrated Test: These tests check the interaction between multiple components or classes to ensure they work together correctly. For example, you might test how a repository interacts with a data source or how a ViewModel interacts with a repository. they are written in the
src/androidTest
directory if they require Android components. If they don't require Android components, they can also be written in thesrc/test
directory. - End-to-End (UI) Tests: These tests simulate real user interactions with the app’s UI. They are used to verify that the entire app works as expected from the user’s perspective. UI tests are written in the
src/androidTest
directory, as they require Android components and need to run on an actual device or emulator.
Now, let's create a class for which we will write tests. The class in question is a PaymentInputValidator that uses whitelist parameters to test inputs to prevent SQL injection and other security vulnerabilities.
class PaymentInputValidator {
// Whitelist of allowed characters
private val allowedCharacters = ('a'..'z') + ('A'..'Z') + ('0'..'9')
/**
* Validates if the input contains only allowed characters (a-z, A-Z, 0-9).
* @param input The input string to validate.
* @return true if the input is valid, false otherwise.
*/
fun isInputValid(input: String): Boolean {
for (char in input) {
if (char !in allowedCharacters) {
return false
}
}
return true
}
/**
* Checks if the input is empty or blank.
* @param input The input string to validate.
* @return true if the input is empty or blank, false otherwise.
*/
fun isEmpty(input: String): Boolean {
return input.isBlank()
}
}
Now, let’s test each function individually to ensure they work as expected. To do this, we’ll follow the AAA (Arrange-Act-Assert) pattern, a structured approach to writing effective tests:
- Arrange: Set up the necessary conditions for the test. For example, create an instance of
PaymentInputValidator
and define the input string to validate (e.g.,"JohnDoe123"
). - Act: Call the method you want to test. For instance, invoking
isInputValid("JohnDoe123")
to check if the input contains only allowed characters. - Assert: Verify the result by comparing the method’s output with the expected value. If
isInputValid("JohnDoe123")
is expected to returntrue
, we check that it does.
Here is what the test code would look like
class PaymentInputValidatorTest {
private lateinit var validator: PaymentInputValidator
@BeforeEach
fun setUp() {
validator = PaymentInputValidator()
}
@Test
fun valid_input_should_return_true() {
// Arrange
val validInput = "JohnDoe123"
// Act
val result = validator.isInputValid(validInput)
// Assert
assertThat(result).isTrue
}
}
Now we couldn't write the test for all possible outcomes because of time but the goal here is to test as many outcomes as possible
Run Your Test: You can run your tests from Android Studio by right-clicking on the test class or using the command line.
Understanding JUnit Annotations
In unit testing with JUnit, various annotations help you manage the test lifecycle and organize your tests. Here’s a quick overview of some essential annotations:
@Before
: TheBefore
annotation ensures that setup initialization runs before each test method. This prevents redundant code and ensures a fresh instance for every test.
@Before
fun setUp() {
validator = PaymentInputValidator()
}
@After
: is used to clean up resources after each test execution. This ensures the value held in any test is properly cleaned up.
@After
fun tearDown() {
// Clean up resources if needed
}
@BeforeClass
: This annotation marks a method that should run once before any tests in the class are executed. It's useful for setup that you want to do only once.
@BeforeClass
fun init() {
// Run once before any tests in this class
}
@AfterClass
: This annotation marks a method that will run once after all tests in the class have finished. It’s useful for final cleanup.
@AfterClass
fun cleanup() {
// Run once after all tests in this class
}
@Test
: This annotation marks a method as a test method. JUnit runs any method annotated with@Test
.
@Test
fun valid_input_should_return_true() {
// Arrange
val validInput = "JohnDoe123"
// Act
val result = validator.isInputValid(validInput)
// Assert
assertThat(result).isTrue
}
By using these annotations, you can effectively organize your test code, and ensure that your tests run as expected.
Conclusion:
Unit testing is a continuous process. Once you have written a unit test you can add more test cases to cover different scenarios and edge cases. for example, if you write more tests you would notice our code has problems with spaces in the input.
Remember, unit testing is just one part of a comprehensive testing strategy. in our next article, we will dive into other different testing techniques to achieve thorough test coverage and build a reliable mobile application.