Blog Infos
Author
Published
Topics
Published
Topics

Alpha alpha! Do you copy?

 

 

Almost all apps work with remote APIs for data fetching, uploading and processing. The most commonly used library for this task is Retrofit. As this has becomes very obvious part of application development, it becomes even more critical to test our api layer to verify our set up.

In this article, we’ll explore how can we write unit test cases for our network layer and make it more and more error free. We’ll be using Retrofit as rest client. So let’s begin.

Retrofit set up

The very first step is to add retrofit dependencies along with gson in our build.gradle as follows

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Gson
implementation 'com.google.code.gson:gson:2.9.0'

Now, we define an interface where we list our apis end point calls as follows

const val BASE_USER_PATH_URL = "/aqua30/users"
interface TestApis {
@GET(BASE_USER_PATH_URL)
suspend fun getAllUsers(): List<User>
@GET("$BASE_USER_PATH_URL/{id}")
suspend fun getUserById(@Path("id") id: Int): User
}
view raw TestApis.kt hosted with ❤ by GitHub
data class User(
val id: Int,
val name: String,
val city: String,
val country: String
)
view raw User.kt hosted with ❤ by GitHub

We’re assuming here that we’ll fetch some users list and a single user by its id.

Then we define a user repository interface to create our user data layer as follows.

interface UserRepository {
suspend fun getAllUsers(): ApiResponse<List<User>>
suspend fun getUserById(id: Int): ApiResponse<User>
}

We’ll now define its implementation as follows.

data class ApiResponse<T>(
val httpCode: Int = HttpURLConnection.HTTP_OK,
val body: T? = null,
val errorMessage: String? = null,
)
view raw ApiResponse.kt hosted with ❤ by GitHub
class UserRepositoryImpl(
private val testApis: TestApis
): UserRepository {
override suspend fun getAllUsers(): ApiResponse<List<User>> {
return try {
ApiResponse(
body = testApis.getAllUsers()
)
} catch (e: HttpException) {
ApiResponse(
httpCode = e.code(),
errorMessage = "server error"
)
} catch (e: IOException) {
ApiResponse(
errorMessage = "connection error"
)
}
}
override suspend fun getUserById(id: Int): ApiResponse<User> {
return try {
ApiResponse(
body = testApis.getUserById(id)
)
} catch (e: HttpException) {
ApiResponse(
httpCode = e.code(),
errorMessage = "server error"
)
} catch (e: IOException) {
ApiResponse(
errorMessage = "connection error"
)
}
}
}

Now if we want to access the user data, it’ll only be accessible vis User Repository. Alright! Now we’re done with our layer set up. Let’s move on to testing set up.

Set up for Testing

The first thing to note is that the test cases will resides in test folder and not in androidTest folder. We don’t have any android related dependency in this layer.

Let’s add some dependencies and understand why we need them.

  • Coroutine test: It provides us support to test coroutines. We get test scopes and dispatchers to run coroutines.
// Coroutine test
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2"
  • JUnit4: It provides us with unit testing framework apis to run tests locally on JVM.
testImplementation 'junit:junit:4.13.2'
  • MockWebServer: It provides support for mocking of the server with which we can actually make api calls and get the output.
// Mock web server
testImplementation 'com.squareup.okhttp3:mockwebserver:4.10.0'

 

  • Truth: It provides support for better assertion and logging of results.
// Google truth for assertion
testImplementation "com.google.truth:truth:1.1.3"

So our dependencies block will now look like as follows.

dependencies {
... other suppport dependencies
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Mock web server
testImplementation 'com.squareup.okhttp3:mockwebserver:4.10.0'
// Gson
implementation 'com.google.code.gson:gson:2.9.0'
// JUnit
testImplementation 'junit:junit:4.13.2'
// Coroutine test
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2"
// Google truth for assertion
testImplementation "com.google.truth:truth:1.1.3"
}

Let’s move on and create a test class by right clicking on the UserRepositoryImpl class name and selecting Create test.

Now once our class is created, we need to set up few variables so that we can write our tests easily.

private lateinit var repository: UserRepository
private lateinit var testApis: TestApis
private lateinit var mockWebServer: MockWebServer
@Before
fun setUp() {
mockWebServer = MockWebServer()
mockWebServer.start()
testApis = RetrofitHelper.testApiInstance(mockWebServer.url("/").toString())
repository = UserRepositoryImpl(testApis)
}
@After
fun tearDown() {
mockWebServer.shutdown()
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

With the advent of Android 15, edge-to-edge design has become the default configuration. Consequently, applications must be capable of accommodating window insets, including the system status bar and navigation bar, as well as supporting drawing…
Watch Video

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android developer
Seven Principles Mobility GmbH

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android develop ...
Seven Principles Mob ...

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android developer
Seven Principles Mobility ...

Jobs

Let’s understand what we did here.

In the @Before annotation, we initialized our repository, api class and mock server objects. We start our mock server and passed the base url from mock web server so that it can hit the mock server and give us the response as expected.

In the @After annotation, we simply shut down the server.

Difference in real and mock server

So why we are mocking a server? This is because mocking a server would give us control on what all kind of responses and requests we want to test our apis handling. As we’re not running tests on a real device and hence don’t have access to a real server. To make our tests functions as expected we’re mocking a real server and MockWebServer would let us do exactly that.

Cool! Now we’re all set to start writing test cases. So let’s begin.

Writing Test Cases
Test Case 1

Let’s write a test wherein we want to verify that if the server has two users and we hit the get all user api, we would get 2 users in return with http code 200.

We run this test in a test scope because we’ve suspend functions.

Line 3–6: So first, we’ll create a list of users which we expect in the server response.

Line 7–9: Create a mock response for the server to return by setting http code and body. So in this case server will return this response no matter which api we hit.

Line 10: Finally we set this mock response to our server.

Line 12: We hit our api end point.

Line 13: We assert if we get exactly same number of users as we set in our server response. We get a true for this.

Line 14: We assert if the list is exactly the same as we set in our server response. We get a true for this.

@Test
fun `for multiple users, api must return all users with http code 200`() = runTest {
val users = listOf(
User(1,"Saurabh","Delhi","India"),
User(2,"Zergain","London","UK"),
)
val expectedResponse = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Gson().toJson(users))
mockWebServer.enqueue(expectedResponse)
val actualResponse = repository.getAllUsers()
assertThat(actualResponse.body).hasSize(2)
assertThat(actualResponse.body).isEqualTo(users)
}

By running our test case, gives us a Pass result.

Test Case 2

Let’s write a test wherein we want to verify that if the server don’t have the user id we pass and we hit the get user by id api, we would get http code 404 and a null body.

Line 3–4: Create a response which we expect in the server response.

Line 5: Set this mock response to our server.

Line 7: Hit the get user by id api.

Line 8: We assert if we get exactly same http code as we set in our server response. We get a true for this.

Line 9: We assert if we get null body as we set in our server response. We get a true for this too.

Note: If we look at our actual function getUserById, we notice that we handled HttpException in which we add http code in our response. Hence we can assert http code in this case.

@Test
fun `for user id not available, api must return with http code 404 and null user object`() = runTest {
val expectedResponse = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
mockWebServer.enqueue(expectedResponse)
val actualResponse = repository.getUserById(1)
assertThat(actualResponse.httpCode).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND)
assertThat(actualResponse.body).isNull()
}

By running our test case, gives us a Pass result.

 

 

Bamn! We verified that our api set up is handling the responses correctly and as expected. Now we can get started and write more test cases for various scenarios and assert if all those are handled correctly.

I would leave that as practice for you to play around and observe the behavior. Let me know if you get stuck somewhere.

Gist For Reference

I’ve added the gist for UserRepositoryImplTest for reference.

@file:OptIn(ExperimentalCoroutinesApi::class)
class UserRepositoryImplTest {
private lateinit var repository: UserRepository
private lateinit var testApis: TestApis
private lateinit var mockWebServer: MockWebServer
@Before
fun setUp() {
mockWebServer = MockWebServer()
mockWebServer.start()
testApis = RetrofitHelper.testApiInstance(mockWebServer.url("/").toString())
repository = UserRepositoryImpl(testApis)
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun `for no users, api must return empty with http code 200`() = runTest {
val users = emptyList<User>()
val expectedResponse = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Gson().toJson(users))
mockWebServer.enqueue(expectedResponse)
val actualResponse = repository.getAllUsers()
assertThat(actualResponse.body).hasSize(0)
}
@Test
fun `for multiple users, api must return all users with http code 200`() = runTest {
val users = listOf(
User(1,"Saurabh","Delhi","India"),
User(2,"Zergain","London","UK"),
)
val expectedResponse = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Gson().toJson(users))
mockWebServer.enqueue(expectedResponse)
val actualResponse = repository.getAllUsers()
assertThat(actualResponse.body).hasSize(2)
assertThat(actualResponse.body).isEqualTo(users)
}
@Test
fun `for server error, api must return with http code 5xx`() = runTest {
val expectedResponse = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_INTERNAL_ERROR)
mockWebServer.enqueue(expectedResponse)
val actualResponse = repository.getAllUsers()
assertThat(actualResponse.httpCode).isEqualTo(HttpURLConnection.HTTP_INTERNAL_ERROR)
}
@Test
fun `for server error, api must return with http code 5xx and error message`() = runTest {
val expectedResponse = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_INTERNAL_ERROR)
mockWebServer.enqueue(expectedResponse)
val actualResponse = repository.getAllUsers()
assertThat(actualResponse.httpCode).isEqualTo(HttpURLConnection.HTTP_INTERNAL_ERROR)
assertThat(actualResponse.errorMessage).isEqualTo("server error")
}
@Test
fun `for user id, api must return with http code 200 and user object`() = runTest {
val mockUser = User(1,"Saurabh","Delhi","India")
val expectedResponse = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Gson().toJson(mockUser))
mockWebServer.enqueue(expectedResponse)
val actualResponse = repository.getUserById(1)
assertThat(actualResponse.httpCode).isEqualTo(HttpURLConnection.HTTP_OK)
assertThat(actualResponse.errorMessage).isNull()
assertThat(actualResponse.body).isEqualTo(mockUser)
}
@Test
fun `for user id not available, api must return with http code 404 and null user object`() = runTest {
val expectedResponse = MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
mockWebServer.enqueue(expectedResponse)
val actualResponse = repository.getUserById(1)
assertThat(actualResponse.httpCode).isEqualTo(HttpURLConnection.HTTP_NOT_FOUND)
assertThat(actualResponse.body).isNull()
}
}
Bonus Read

In case you want to read about testing various layers of your app. Do checkout below links.

That is all for now! Stay tuned!

Connect with me on medium(if the content is helpful to you) or on github and subscribe to email to be in sync for further interesting topics on Android/IOS/Backend/Web.

Until next time…

Cheers!

This article was originally published on proandroiddev.com on August 23, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Every good Android application should be well tested to minimize the risk of error…
READ MORE
blog
In this article we’ll go through how to own a legacy code that is…
READ MORE
blog

Running Instrumented Tests in a Gradle task

During the latest Google I/O, a lot of great new technologies were shown. The…
READ MORE
Menu