Skip to content
This repository has been archived by the owner on Jun 14, 2023. It is now read-only.

Unit Tests improvements #137

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ dependencies {
testImplementation(libs.test.robolectric)
testImplementation(libs.ktor.mock)
testImplementation(libs.test.mockk)
testImplementation(libs.google.truth)
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.Flow

interface TokenProvider {

suspend fun fetch(): Flow<String?>
suspend fun fetch(): Flow<String?> // @Allan -> Don't suspend Flows
Copy link
Contributor

@chepsi chepsi Oct 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment. We can add it to the readme instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anselmoalexandre Good catch

Remove the suspend and return a flow 👯

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @chepsi
This comment is just a heads up to @jumaallan so he can refactor this. @jumaallan please check this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anselmoalexandre saw it

I can raise a PR, but if you want, please feel free to refactor it here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anselmoalexandre saw it

I can raise a PR, but if you want, please feel free to refactor it here

You can do the PR it's okay 👌🏿. Also, this was only heads up 😄


suspend fun update(accessToken: String)
}
62 changes: 24 additions & 38 deletions data/src/test/java/com/android254/data/dao/SessionDaoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,61 +15,47 @@
*/
package com.android254.data.dao

import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.android254.data.db.Database
import com.android254.data.db.model.Session
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import com.google.common.truth.Truth
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.IOException

@RunWith(RobolectricTestRunner::class)
class SessionDaoTest {

private lateinit var session: Session
@MockK
private lateinit var sessionDao: SessionDao
private lateinit var db: Database
Comment on lines -35 to -39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using Robolectric because we want to test against a real db.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michaelbukachi With robolectric: it slows down the tests a lot, but it's sometimes helpful if we really want to test the integration with the Android system

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest to use it only in a few places, not everywhere. Considering that tests performance are also very important and should be fast as possible

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using Robolectric because we want to test against a real db.

But it's okay if you guys think you want to stick with this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anselmoalexandre We are aware of its drawbacks. We used it to ensure all our queries were running as expected.


@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context,
Database::class.java
)
.allowMainThreadQueries() // TODO Please delete me
.build()
sessionDao = db.sessionDao()
}

@After
@Throws(IOException::class)
fun tearDown() {
db.close()
}

@Test
fun `test sessionDao fetches all sessions`() = runTest {
val session = Session(
MockKAnnotations.init(this, relaxUnitFun = true)
session = Session(
id = 0,
title = "Retrofiti: A Pragmatic Approach to using Retrofit in Android",
description = "This session is codelab covering some of the best practices and recommended approaches to building an application using the retrofit library.",
slug = "retrofiti-a-pragmatic-approach-to-using-retrofit-in-android-1583941090",
session_format = "Codelab / Workshop",
session_level = "Intermediate",
)

coJustRun { sessionDao.insert(session) }
coEvery { sessionDao.fetchSessions() } returns flow { emit(listOf(session)) }
}

@Test
fun `test sessionDao fetches all sessions`() = runTest {
// Given
sessionDao.insert(session)
runBlocking {
val result = sessionDao.fetchSessions().first().first()
assertThat(session.title, `is`(result.title))
}

// When
val result = sessionDao.fetchSessions().first()

// Then
coVerify(atLeast = 1) { sessionDao.insert(session) }
coVerify { sessionDao.insert(session) }
Truth.assertThat(result.first().title).isEqualTo(session.title)
}
}
50 changes: 30 additions & 20 deletions data/src/test/java/com/android254/data/network/AuthApiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ import com.android254.data.network.models.responses.UserDetails
import com.android254.data.network.util.HttpClientFactory
import com.android254.data.network.util.ServerError
import com.android254.data.preferences.DefaultTokenProvider
import com.google.common.truth.Truth
import io.ktor.client.engine.mock.*
import io.ktor.http.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -55,19 +54,21 @@ class AuthApiTest {

@Test(expected = ServerError::class)
fun `test ServerError is thrown when a server exception occurs`() {
// Given
val mockEngine = MockEngine {
delay(500)
respondError(HttpStatusCode.InternalServerError)
}
val httpClient = HttpClientFactory(DefaultTokenProvider(testDataStore)).create(mockEngine)
val api = AuthApi(httpClient)
runBlocking {
api.logout()
}

// When
runTest { api.logout() }
}

@Test
fun `test successful logout`() {
// Given
val mockEngine = MockEngine {
respond(
content = """{"message": "Success"}""",
Expand All @@ -77,14 +78,18 @@ class AuthApiTest {
}
val httpClient = HttpClientFactory(DefaultTokenProvider(testDataStore)).create(mockEngine)
val api = AuthApi(httpClient)
runBlocking {
val response = api.logout()
assertThat(response, `is`(Status("Success")))

// Then
runTest {
api.logout().also {
Truth.assertThat(it).isEqualTo(Status("Success"))
}
}
}

@Test
fun `test successful google login`() {
// Given
val content = """
{
"token": "test",
Expand All @@ -106,18 +111,23 @@ class AuthApiTest {
}
val httpClient = HttpClientFactory(DefaultTokenProvider(testDataStore)).create(mockEngine)
val api = AuthApi(httpClient)
runBlocking {
val accessToken = AccessToken(
token = "test",
user = UserDetails(
name = "Magak Emmanuel",
email = "[email protected]",
gender = null,
avatar = "http://localhost:8000/upload/avatar/img-20181016-wa0026jpg.jpg"
)

val accessToken = AccessToken(
token = "test",
user = UserDetails(
name = "Magak Emmanuel",
email = "[email protected]",
gender = null,
avatar = "http://localhost:8000/upload/avatar/img-20181016-wa0026jpg.jpg"
)
val response = api.googleLogin(GoogleToken("some token"))
assertThat(response, `is`(accessToken))
)

// Then
runTest {
api.googleLogin(GoogleToken("some token")).also {
Truth.assertThat(it).isEqualTo(accessToken)
}
}

}
}
98 changes: 57 additions & 41 deletions data/src/test/java/com/android254/data/repos/AuthManagerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import com.android254.data.network.util.NetworkError
import com.android254.data.network.util.TokenProvider
import com.android254.domain.models.DataResult
import com.android254.domain.models.Success
import com.google.common.truth.Truth
import io.mockk.*
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import java.lang.Exception

class AuthManagerTest {
private val fakeUserDetails = UserDetails(
Expand All @@ -37,49 +37,65 @@ class AuthManagerTest {
avatar = "http://test.com"
)

@MockK
private lateinit var authApi: AuthApi

@MockK
private lateinit var tokenProvider: TokenProvider

private lateinit var authManager: AuthManager
private lateinit var exception: Exception
private lateinit var networkError: NetworkError

@Before
fun setup() {
MockKAnnotations.init(this, relaxUnitFun = true)
authManager = AuthManager(authApi, tokenProvider)
networkError = NetworkError()
exception = Exception()
}

@Test
fun `test getAndSaveApiToken successfully`() {
val mockApi = mockk<AuthApi>()
val mockTokenProvider = mockk<TokenProvider>()

runBlocking {
val repo = AuthManager(mockApi, mockTokenProvider)
coEvery { mockApi.googleLogin(any()) } returns AccessToken("test", user = fakeUserDetails)
coEvery { mockTokenProvider.update(any()) } just Runs

val result = repo.getAndSaveApiToken("test")
assertThat(result, `is`(DataResult.Success(Success)))
coVerify { mockTokenProvider.update("test") }
}
fun `test getAndSaveApiToken successfully`() = runTest {
// Given
coEvery { authApi.googleLogin(any()) } returns AccessToken(
"test",
user = fakeUserDetails
)
coEvery { tokenProvider.update(any()) } just Runs

// When
val result = authManager.getAndSaveApiToken("test")

//Then
coVerify { tokenProvider.update("test") }

// And
Truth.assertThat(result).isEqualTo(DataResult.Success(Success))
}

@Test
fun `test getAndSaveApiToken failure - network error`() {
val mockApi = mockk<AuthApi>()
val mockTokenProvider = mockk<TokenProvider>()

runBlocking {
val repo = AuthManager(mockApi, mockTokenProvider)
val exc = NetworkError()

coEvery { mockApi.googleLogin(any()) } throws exc
val result = repo.getAndSaveApiToken("test")
assertThat(result, `is`(DataResult.Error("Login failed", true, exc)))
}
fun `test getAndSaveApiToken failure - network error`() = runTest {
// Given
val exc = NetworkError()
coEvery { authApi.googleLogin(any()) } throws networkError

// When
val result = authManager.getAndSaveApiToken("test")

// Then
Truth.assertThat(result).isEqualTo(DataResult.Error("Login failed", true, networkError))
}

@Test
fun `test getAndSaveApiToken failure - other error`() {
val mockApi = mockk<AuthApi>()
val mockTokenProvider = mockk<TokenProvider>()

runBlocking {
val repo = AuthManager(mockApi, mockTokenProvider)
val exc = Exception()

coEvery { mockApi.googleLogin(any()) } throws exc
val result = repo.getAndSaveApiToken("test")
assertThat(result, `is`(DataResult.Error("Login failed", exc = exc)))
}
fun `test getAndSaveApiToken failure - other error`() = runTest {
// Given
coEvery { authApi.googleLogin(any()) } throws exception

// When
val result = authManager.getAndSaveApiToken("test")

// Then
Truth.assertThat(result).isEqualTo(DataResult.Error("Login failed", exc = exception))
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" }
firebase-performance = { module = "com.google.firebase:firebase-perf-ktx" }
google-truth = "com.google.truth:truth:1.0.1"

[bundles]
compose = ["coil-compose", "compose-activity", "compose-compiler", "compose-material-3", "compose-materialIcons", "compose-runtimeLivedata", "compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview", "paging-compose", "compose-preview-customview", "compose-preview-customview-poolingcontainer", "compose-constraintlayout"]
Expand Down