Android Unit Testing Clean Code Architecture with MVVM

Sreehari Vasudevan
8 min readDec 20, 2019

Unit testing Android Application while libraries getting upgraded and added into Android eco system is definitely a challenge.😱

Similar is the story with Android + MVVM + Uncle Bob’s Clean Code Architecture.

This post is targeted to address data flow with Clean Code Architecture and MVVM along with testing (UT + UI aka Instrumentation Testing)

Main Libraries Used

Full Source code can be found in this Github Project

Here I will be mainly targeting how to test the data flow with respect to different components in the app. Which are the following points. Anybody can easily adopt the following technique in apps. Even for complex apps, underlying idea can remain the same.😇

  1. View Model ( or consider this as an Interactor from View. Responsible for taking inputs from View and delivering back the processed response)
  2. Use Case ( There can be one or multiple use case flows in single View Model. Handles specific data manipulation for a flow and connecting with Repository for the same)
  3. Repository ( Either connects to REST API or Data Base)

Note: REST API provided by https://swapi.dev will be used in this article for showing sample data retrieval. Thanks to SWAPI.

The complete API documentation detail can be found over https://swapi.dev/documentation

Application Architecture

Let us target to consider a common data flow.

User launches app. App hits a URL and get back data. With which View populates a List View.

From the Android Guide to app architecture , slight change for use case can get added for the separation of concerns. Which will give us the following architecture diagram. This article will be discussing the development details of the below architecture along with testing.

Clean Code Architecture with MVVM
List View displaying data from SWAPI

Targeted screen/fragment, view will have corresponding View Model class instance incorporated through Kotlin lazy. A method in ViewModel(VM) will be invoked to initiate data retrieval with few/no parameters.

VM will have reference to the respective Use Case class passed from Fragment received through Dependency Injection. With this reference , VM will invoke method in Use Case to execute corresponding use case flow (Eg:- Login request).

Use case will receive this call, if any business logic needs to be performed, use case will handle and similarly ,with the help of Repository reference received via Dependency Injection, use case invokes method in repository for getting data.

Repository will in-turn get in touch with Network API ( Retrofit/ Any similar Networking Client) and retrieves the data. Or connect to local Data Base and retrieves data.

NOTE: In this flow, nowhere we are having cyclic reference of other components.

Now, getting back to the data flow. Once Repository receives data, it will decide whether data needs to be saved in local DB (Eg:- Room ORM) or just get back to caller. If the data needs to be reverted back to caller, the same will get passed over to View through the above explained flow in reverse order😀. Kotlin coroutines makes this flow relatively simple with help of suspend functions as shown below.

Important dependencies for the project

build.gradle will have the following. Complete dependencies can be found in Github Project

//Koin
implementation "org.koin:koin-android:$XX"
//Retrofit
implementation "com.squareup.retrofit2:retrofit:$XX"
//Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$XX"
//Material
implementation "com.google.android.material:material:$XX"
//Mockk
testImplementation "io.mockk:mockk:$XX"
//Koin Test
androidTestImplementation "org.koin:koin-test:$XX"
//Mockwebserver
androidTestImplementation "com.squareup.okhttp3:mockwebserver:$XX"

Dependency Injection with Koin

First part is to get our Dependency Injection (DI) targets and corresponding classes ready to be injected.

  • Network ( Retrofit)
  • View Model
  • Use Case
  • Repository

All these dependencies will be injected to main application class as follows

MainApp.kt

class MainApp : MultiDexApplication() {

override fun onCreate() {
super.onCreate()
initiateKoin()
}

private fun initiateKoin() {
startKoin{
androidContext(this@MainApp)
modules(provideDependency())
}
}

open fun provideDependency() = appComponent
}

MainDIComponent.kt

Concludes all dependencies required throughout the application. (List of Koin Dependencies )

val appComponent = listOf(UseCaseDependency, AppUtilDependency, NetworkDependency, SharedPrefDependency, RepoDependency)

LoginActivityFragment.kt

In view , the View Model will be injected and used as follows.

private val mViewModel : LoginActivityViewModel by lazy 
{
ViewModelProviders.of(this,mBaseViewModelFactory)
.get(LoginActivityViewModel::class.java)
}

LoginActivityViewModel.kt

View Model will have Use Case as dependent class received from corresponding Fragment. Which will invoke specific method as shown below.

fun requestLoginActivityData(param:String) {
if(mAllPeopleResponse.value == null){
mUiScope.launch {

//.....

val data = mIoScope.async {
return@async useCase.processLoginUseCase(param)
}.await()
          //.....
      }
}
}

LoginUseCase.kt

Use Case will pass on the incoming request to Repository and handle any business logic required once data gets back from Repository.

suspend fun processLoginUseCase(query: String) : AllPeople {
//Data manipulation required prior
   val response =  mLoginRepo.getLoginData(query)
   //Data manipulation after receiving response    
return response
}

LoginRepository.kt

Repository will initiate Network Request and get back data from REST End Point.

suspend fun processDataFetchLogic(parameter:String): AllPeople{

//Data manipulation prior to REST API request if required

val dataReceived = mLoginAPIService.getLoginData(parameter)

//Data manipulation post REST API request if required

return dataReceived
}

LoginAPIService.kt

@GET("people/")
suspend fun getLoginData(@Query("page") page:String): AllPeople

The data received will be send back to UseCase. UseCase, as mentioned earlier will handle any business logic required and revert back processed response to ViewModel. ViewModel will update the live data object, which will be observed by View class. View in turn updates the data correspondingly.

Unit Testing

So now, coming to testing. 😵 Questions which would arise are. 🤔

  1. What classes need to be tested.
  2. How can dependencies be 💉injected for testing.
  3. How response will be mocked? If then what libraries needed to be used.
  4. How will the mocked response be accessible to test classes.
  5. Do I need to check only flow till my business logic hits Retrofit classes or should I test Retrofit classes as well.

As the post is concentrating on clean code +MVVM data flow testing, I will concentrate on data flow areas only to keep the article less complicated. Ideally all the written code should be tested. By following TDD (Test Driven Development) all other classes developed can be targeting for testing. Moreover neither developer will implement logic that is not testable nor include any code which is not required to satisfy the requirements.

First and foremost point.

❌🛑 In the mentioned data flow ,no classes other than Fragment/Activity should have reference to view.❌🛑

Having references of view in ViewModel or other classes are not at all encouraged. Moreover this makes the code Non Testable.

  1. Getting the dependencies ready for testing

In-order to override and get specific dependencies for testing, we can create similar dependencies in test folder and make it available for test classes through following method.

startKoin{ modules( .....)}

Through MainDIcomponentTest.kt we can have custom network dependency, MockWebServer and others injected to test classes using Koin. Test classes can invoke this as well.

fun configureTestAppComponent(baseApi: String)
= listOf(
MockWebServerDIPTest,
configureNetworkModuleForTest(baseApi),
UseCaseDependency,
RepoDependency
)

The NetworkDITest.kt will provide network dependency were we can pass the URL while testing with MockWebServer specific endpoint.

fun configureNetworkModuleForTest(baseApi: String)
= module{
single {
Retrofit.Builder()
.baseUrl(baseApi)
.addConverterFactory (GsonConverterFactory
.create (GsonBuilder().create()))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
}
factory{ get<Retrofit>().create(LoginAPIService::class.java) }
}

With these, our dependencies for Unit Testing is completed.

2. Testing Repository Class

For unit testing, we will create a classes for basic configuration’s. Each unit test class will extend this base class which intern will be extending KoinTest class. Refer BaseUTTest.kt for the details.

While requesting for REST API , we can get back mocked response using MockWebServer. This response will be received from locally saved json file.

Json file for Unit Testing
val mNextValue = "https://swapi.co/api/people/?page=2"
val mParam = "1"
val mCount = 87
@Test
fun test_login_repo_retrieves_expected_data() = runBlocking<Unit>{

mockNetworkResponseWithFileContent("success_resp_list.json", HttpURLConnection.HTTP_OK)
mRepo = LoginRepository()

val dataReceived = mRepo.getLoginData(mParam)

assertNotNull(dataReceived)
assertEquals(dataReceived.count, mCount)
assertEquals(dataReceived.next, mNextValue)
}

Compare the expectation with received response (Assertion) . We can see that test is passed and UT gives us a ✔️ 😍 Hoorrraaayyyy 🎉🕺🏆

Login Repository Test case passed.

3. Testing Use Case class

In the similar lines, we can target LoginUseCase’s processLoginUseCase method for testing.

val mNextValue = "https://swapi.co/api/people/?page=2"
val mParam = "1"
val mCount = 87
@Test
fun test_login_use_case_returns_expected_value()= runBlocking{

mockNetworkResponseWithFileContent("success_resp_list.json", HttpURLConnection.HTTP_OK)
mLoginUseCase = LoginUseCase()

val dataReceived = mLoginUseCase.processLoginUseCase(mParam)

assertNotNull(dataReceived)
assertEquals(dataReceived.count, mCount)
assertEquals(dataReceived.next, mNextValue)
}

We can get this test case also passed and gives us ✔️ 😍

Login Use Case Test Pass

3. Testing View Model class

Here we will use Mockk’s libraries coEvery method for mocking the expected response. To initiate mockk use the following.

MockKAnnotations.init(this)

And now we can have our test case with the following. And run the test.

val mParam = "1"
val mNextValue = "https://swapi.co/api/people/?page=2"
@Test
fun test_login_view_model_data_populates_expected_value(){

mLoginActivityViewModel = LoginActivityViewModel
(mDispatcher,mDispatcher,mLoginUseCase)
val sampleResponse = getJson("success_resp_list.json")
var jsonObj = Gson().fromJson(sampleResponse,
AllPeople::class.java)
//Make sure login use case returns expected response when called
coEvery { mLoginUseCase.processLoginUseCase(any()) }
returns jsonObj
mLoginActivityViewModel.mAllPeopleResponse.observeForever {}

mLoginActivityViewModel.requestLoginActivityData(mParam)

assert(mLoginActivityViewModel.mAllPeopleResponse.value != null)
assert(mLoginActivityViewModel.mAllPeopleResponse.value!!.
responseStatus == LiveDataWrapper.
RESPONSESTATUS.SUCCESS)
val testResult = mLoginActivityViewModel.mAllPeopleResponse
.value as LiveDataWrapper<AllPeople>
assertEquals(testResult.response!!.next,mNextValue)
}
View Model test case passed

This completes testing the data flow of Clean Code Architecture.

I have kept the architecture as simple as possible for a beginner to grasp and start with. There are improvements and abstraction possible in many areas, which you can try out😉

With this I will give a pause and continue Instrumentation testing part in next post !

Part 2 : Instrumentation Testing Clean Code + MVVM

Do clap 👏 if you find this article helpful.

Full Source code can be found in this Github Project

Thanks for Reading ! Happy Coding 🎉Cheers 🤜🤛🥂

All gif images copyright https://giphy.com/

--

--