Android Project Architecture for Instrumented Testing with Kotlin/Java

Wai Hein
8 min readSep 29, 2019

--

First, a bit about me. I am an experienced Software Engineer who previously worked as a Mobile Application Developer, Full-Stack Software Engineer, Virtual Reality and Augmented Reality Developer, and Cloud Engineer.

Kotlin and Testing in Android

Test Pyramid

In Kotlin/Java native Android development, integration tests are commonly referred to as instrumented tests. Designing a project structure that facilitates efficient instrumented testing is a challenging task. At times, it becomes necessary to mock certain logic. In my scenario, I’m working with the GraphQL API using the Apollo client, requiring me to mock the logic for making API requests. Apollo API calls in Android operate asynchronously. Mocking asynchronous logic presents challenges in Android, especially when we aim to avoid mocking the logic of the asynchronous callback while mocking the method executing the asynchronous call.

This article outlines the step-by-step approach to the architecture and project structure I employ in my Android projects, along with the considerations that led me to adopt this structure. I primarily use the Kotlin programming language, so I assume you are already familiar with Kotlin. It’s worth noting that Kotlin has been officially endorsed by Google as the preferred programming language for Android development, superseding Java.

Hello…… It’s me.

This is how it all begins….

“One day,” I began working on a Kotlin Android project and started implementing a Login feature. In my LoginActivity class, I had to consume the GraphQL API.

Here is the dummy code for my LoginActivity class.

class LoginActivity : AppCompatActivity()
{
//some code hidden
.
.
.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// some code hidden ....
view.login_btn_submit.setOnClickListener {
this.handleLoginButtonClick()
}
} fun handleLoginButtonClick()
{
//some code hidden here
.
.
.
val mutation = ... // build the login mutation here

apolloClient.mutate(mutation).enqueue(object: ApolloCall.Callback<LoginMutation.Data>() {
override fun onFailure(e: ApolloException) {
text_view_error_message.text = "There was an error logging in"
}

override fun onResponse(response: Response<LoginMutation.Data>) {
startMainActivity()
}
})
}
}

The above code functions well until you commence writing tests for it. For enthusiasts of test-driven development, the problem becomes apparent. There is a need to mock the logic of making the API call during the test wiring. The following was the initial design that struck me:

The instance of the AuthService will be injected into the LoginActivity class.

To make the code fully testable, my initial approach involved refactoring the login API request code into a separate class. Subsequently, this class would be declared as a property of the LoginActivity class. The AuthenticationService class would then be injected into the LoginActivity class using dependency injection. This way, I could mock the AuthenticationService class in my tests and manipulate its behavior.

Here is the dummy code for my AuthenticationService class:

class AuthenticationService: IAuthenticationService
{
fun login(email: String, password: String, view: View, activity: Activity) {
val mutation = ... // build the login mutation here

apolloClient.mutate(mutation).enqueue(object: ApolloCall.Callback<LoginMutation.Data>() {
override fun onFailure(e: ApolloException) {
view.text_view_error_message.text = "There was an error logging in"
}

override fun onResponse(response: Response<LoginMutation.Data>) {
activity.startActivity(Intent(pass the parameters here to start new activity)) // this is just dummy code
}
})
}
}

The class extends the IAuthenticationService interface (you can infer the structure of IAuthenticationService). Additionally, the LoginActivity class will have a class property with the type IAuthenticationService. As you may have already noticed, I am implementing dependency injection.

However, there are two apparent issues with the above class that need addressing.

2. It violates the single responsibility of the SOLID principles

We are passing the view and activity parameters into the login method. However, the AuthenticationService class should not be responsible for dealing with UI and navigation flow. This violates the first rule of SOLID - the Single Responsibility Principle. A class should exhibit high cohesion and focus on a single responsibility.

2. The asynchronous nature is sometimes problematic for writing tests.

The asynchronous nature of the method presents a challenge because the completion of its execution is unpredictable. I must emulate this behavior in my tests. Even if I can mock the AuthenticationService class and its login method, how can I ensure in my test that it updates the UI as expected within the callback method of the asynchronous call? If I am mocking the logic within the asynchronous callback method, the purpose of writing instrumented/integrated tests for my project becomes questionable.

If I were to mock the entire login method of the IAuthenticationService interface, the purpose of adding instrumented tests for the LoginActivity class becomes questionable. This is because I would have to mock the logic of updating the UI (view and activity parameters) as well, thereby compromising the robustness of regression tests. How can I effectively mock the behavior of the login method to ensure that the instrumented tests are written efficiently, covering every possible aspect of the implementation?

I had to refactor the view and activity parameters into the LoginActivity class and delegate the UI tasks to it. By doing so, when writing tests, I could mock the login method to trigger an event that can be listened to within the LoginActivity class. The LoginActivity class would then update the UI within the event listener, allowing me to replicate the behavior of the asynchronous call.

How can I achieve that?

Solution:

I implemented the solution by employing a reactive programming approach called RxJava/RxKotlin.

If you want to learn more about RxJava, you can visit this link: RxJava Tutorial. Since I am using Kotlin, I refer to it as RxKotlin. RxKotlin is essentially a ReactiveX programming API tailored for Kotlin. But what does that mean? It’s an API designed for asynchronous programming with observable streams. Now, what exactly is an observable stream? Sometimes, I tend to simplify definitions for better understanding.

ReactiveX, in my understanding, is akin to “an observer pattern that patiently awaits something to happen. When that event occurs, the event listener elsewhere reacts to it and performs actions based on the event.” In my scenario, the AuthenticationService runs an asynchronous call. Upon completion, it triggers an event. The LoginActivity class is set to listen for that event and take actions based on the message (data) sent from the triggered event.

Time to return to coding. I registered an observer and observable in the LoginActivity class.

class LoginActivity: AppCompatActivity()
{
//some code hidden
.
.
companion object {
lateinit var loginObservable: Observable<DataModel>
lateinit var loginObserver: Observer<DataModel>
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// some code hidden ....
loginObservable = Observable.empty()
loginObserver = setUpLoginObserver()
loginObservable.subscribe(loginObserver)
view.login_btn_submit.setOnClickListener {
this.handleLoginButtonClick()
}
} private fun setUpLoginObserver(): Observer<DataModel> {
return object: Observer<DataModel> {
override fun onSubscribe(d: Disposable) {
//do something
}

override fun onNext(t: DataModel) {
//do something
}

override fun onError(e: Throwable) {
//do something
}

override fun onComplete() {
//do something
}
}
}
}

NOTE: DataModel is a custom class I created to pass data from the service class to the activity class.

As evident in the code, I created two fields within the companion object. If you are coming from a Java background, you can perceive them as static properties. These are the observable and the observer. Subsequently, the observer subscribes to the observable. Consequently, when something happens (changes) in the observable, the observer will execute one of the callbacks (as seen in the setUpLoginObserver method) based on the event.

Here is the updated version of the dummy code for the AuthenticationService class.

class AuthenticationService: IAuthenticationService
{
fun login(email: String, password: String) {
val mutation = ... // build the login mutation here

apolloClient.mutate(mutation).enqueue(object: ApolloCall.Callback<LoginMutation.Data>() {
override fun onFailure(e: ApolloException) {

}

override fun onResponse(response: Response<LoginMutation.Data>) {
//pass your data in the data parameter (DataModel) class type
LoginActivity.loginObserver.onNext(data)
}
})
}
}

As evident in the code, within the success callback of the asynchronous call, it triggers the onNext callback of the observer. The LoginActivity will execute the onNext callback and update the UI. Now, I can create a mock version of the AuthenticationService class to be used in the test. With this mock class, I can simulate the behavior of the asynchronous call and trigger the onNext event of the observer.

NOTE: I am utilizing Dagger 2 and Espresso for my instrumented tests. If you wish to delve into Dagger 2, I recommend this article: Dagger 2 for Dummies in Kotlin with One Page Simple Code Project.

I created a mock version of the AuthenticationService class, named FakeAuthenticationService, with the following dummy code.

class FakeAuthenticationService: IAuthenticationService
{
companion object {
var SCENARIO_UNDER_TEST = 0
val SCENARIO_LOGIN_ERROR: Int = 1
val SCENARIO_LOGIN_SUCCESSFUL: Int = 2
}

override fun login(email: String, password: String) {
//Maybe run the code with a timeout if you want it to be asynchronous
val dataModel = DataModel()
when (SCENARIO_UNDER_TEST) {
SCENARIO_LOGIN_ERROR -> {
dataModel.message = "Unable to login"
}
SCENARIO_LOGIN_SUCCESSFUL -> {
loginModel.message = "Login successful"
loginModel.accessToken = "fake-access-token"
}
}
LoginActivityt.loginObserver.onNext(dataModel)
}
}

As evident in the code, it still implements the IAuthenticationService interface, allowing me to use this class instead of the AuthenticationService class for my tests—thanks to "Dependency Injection." The code showcases how I mocked the login method to behave according to the scenario I am testing. Below is how I tested a scenario for the login feature.

As visible, I am testing whether the error message is displayed when the request throws an error. I define the scenario I want to test at the outset of the test. I plan to create a separate article to cover configuring the tests and mocking classes with Dagger 2, as that is where the magic happens.

This is the workflow of my project.

Workflow for running async task

Optional: making it better using MVVM pattern

However, I would like to introduce another element to the current architecture to enhance the project’s maintainability and organization. As you can observe, I created a class called DataModel to facilitate data transfer between the observable and the observer. I have renamed this class to LoginViewModel. The LoginViewModel class will now serve as the view model for the LoginActivity. I assume you are already familiar with the View Model component in the MVVM pattern for mobile application development, as described here: Model–view–viewmodel.

Below is the project structure I ultimately adopted, which is fully conducive to instrumented testing.

This article solely addresses the design of an Android project structure or architecture that is fully testable through instrumented/integrated tests. I plan to create a separate article focused on configuring Dagger 2 to mock the classes, providing more detailed insights into the codebase. I trust you find this article helpful, especially if you are endeavoring to establish an Android project that seamlessly integrates with instrumented tests.

--

--

Wai Hein
Wai Hein

Written by Wai Hein

8 x AWS Certified Full-stack | DevOps | Serverless Engineer with over a decade of experience

Responses (1)