Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Unit tests/basics #28

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions buildSrc/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ dependencyResolutionManagement {
create("appLibs") {
from(files("../gradle/dependencies.toml"))
}
create("testLibs") {
from(files("../gradle/testDependencies.toml"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.tapptitude.core.usecase
import com.tapptitude.core.model.Image
import com.tapptitude.core.repository.ImageRepository

class LoadImageUseCase internal constructor(private val imageRepository: ImageRepository) {
class LoadImageUseCase constructor(private val imageRepository: ImageRepository) {

suspend fun invoke(): Image = imageRepository.getRandomImage()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't it also have the operator modifier? 🤔

}
3 changes: 3 additions & 0 deletions featureHome/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ dependencies {
implementation(appLibs.bundles.androidXLifecycleBundle)
implementation(appLibs.bundles.androidXNavigationBundle)
implementation(appLibs.bundles.koinBundle)

testImplementation(testLibs.kotlinXCoroutines)
testImplementation(testLibs.bundles.testBundle)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class HomeViewModel(
private val loadImageUseCase: LoadImageUseCase,
private val sessionManager: SessionManager
) : ViewModel() {
private val _isLoading = MutableLiveData<Boolean>()
private val _isLoading = MutableLiveData<Boolean>(false)
val isLoading: LiveData<Boolean> = _isLoading
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is supposed to have a getter instead of assigned MutableLiveData value, right?


private val _imageData = MutableLiveData<Image>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.tapptitude.featurehome

import com.tapptitude.core.usecase.LoadImageUseCase
import com.tapptitude.featurehome.fake.FAKE_IMAGE
import com.tapptitude.featurehome.fake.FakeImageRepository
import com.tapptitude.featurehome.fake.FakeSessionManager
import com.tapptitude.featurehome.presentation.HomeViewModel
import com.tapptitude.session.SessionManager
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

internal class HomeViewModelTest {

private val loadImageUsecase: LoadImageUseCase = LoadImageUseCase(
imageRepository = FakeImageRepository(requestDelay = 0, fakeImage = FAKE_IMAGE)
)
private lateinit var fakeSessionManager: SessionManager
private lateinit var viewModel: HomeViewModel

@Before
fun setup() {
fakeSessionManager = FakeSessionManager()

viewModel = HomeViewModel(
loadImageUseCase = loadImageUsecase,
sessionManager = fakeSessionManager,
)
}

@After
fun tearDown() = Unit

@Test
fun `initial state is not loading`() {
assertEquals(
false,
viewModel.isLoading.value,
)
}

@Test
fun `assert no image is present at start`() {
// We do not make any request. We shouldn't have any image
assertEquals(
null,
viewModel.imageData.value,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.tapptitude.featurehome.fake

import com.tapptitude.core.model.Image

internal val FAKE_IMAGE = Image("test_url")
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.tapptitude.featurehome.fake

import com.tapptitude.core.model.Image
import com.tapptitude.core.repository.ImageRepository
import kotlinx.coroutines.delay


internal class FakeImageRepository(
var requestDelay: Long = 0,
private val fakeImage: Image = Image("test_url"),
) : ImageRepository {

override suspend fun getRandomImage(): Image {
delay(requestDelay)
return fakeImage
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.tapptitude.featurehome.fake

import com.tapptitude.session.SessionManager
import com.tapptitude.session.model.LoggedIn
import com.tapptitude.session.model.LoggedOut
import com.tapptitude.session.model.LoginState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

internal class FakeSessionManager : SessionManager {
private val _currentLoginStateFlow: MutableStateFlow<LoginState> = MutableStateFlow(
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the Hard Wrap setting value in your Android Studio? I think anything under ~120 is a bit too short

LoggedOut("")
)

override val currentLoginStateFlow: StateFlow<LoginState>
get() = _currentLoginStateFlow

override fun onLoggedIn(accessToken: String, userId: String) {
_currentLoginStateFlow.value = LoggedIn(
sessionToken = accessToken,
userId = userId
)
}

override fun onLoggedOut(lastUserId: String?) {
_currentLoginStateFlow.value = LoggedOut(
null
)
}
}
4 changes: 4 additions & 0 deletions gradle/testDependencies.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
[versions]
junit = "4.13.2"
mockWebServer = "4.10.0"
kotlinX = "1.6.4"

[libraries]
junit = { module = "junit:junit", version.ref = "junit" }
mockWebServer = {module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockWebServer" }
kotlinXCoroutines = {module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinX"}

[bundles]
testBundle = [
Expand Down
5 changes: 4 additions & 1 deletion network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ dependencies {
implementation(appLibs.kotlinXCoroutines)
implementation(appLibs.moshi)
ksp(appLibs.moshiKsp)
}

testImplementation(testLibs.bundles.testBundle)
testImplementation(testLibs.mockWebServer)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tapptitude.network.interceptor

import androidx.annotation.VisibleForTesting
import com.tapptitude.session.SessionManager
import com.tapptitude.session.model.LoggedIn
import okhttp3.Interceptor
Expand All @@ -21,6 +22,7 @@ internal class SessionInterceptor(private val sessionManager: SessionManager) :
}

companion object {
private const val HEADER_AUTHORIZATION = "Authorization"
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val HEADER_AUTHORIZATION = "Authorization"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.tapptitude.network

import com.tapptitude.network.fake.FakeSessionManager
import com.tapptitude.network.fake.IgnoredCallback
import com.tapptitude.network.interceptor.SessionInterceptor
import com.tapptitude.session.SessionManager
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

/**
* Underneath, Retrofit2 uses OkHttp for requests.
*
* Thus, if we want to test an interceptor, it is enough to use a OkHttp object.
* No need to create an instance of retrofit
*/
internal class SessionInterceptorTest {
private lateinit var interceptor: SessionInterceptor
Copy link
Collaborator

Choose a reason for hiding this comment

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

I prefer leaving an empty line between class declaration and first field/method. Makes it feel a bit cleaner :D

private lateinit var sessionManager: SessionManager
private lateinit var okHttpClient: OkHttpClient
private lateinit var mockWebServer: MockWebServer

@Before
fun `before each test, do a fresh set up`() {
sessionManager = FakeSessionManager()
interceptor = SessionInterceptor(sessionManager)
okHttpClient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()

mockWebServer = MockWebServer()
mockWebServer.start()
}

@After
fun `after each test, clean up the resource`() {
mockWebServer.shutdown()
}

@Test
fun `assert when logged in the authorization header is set`() {
val mockUserId = "mocked_user_id"
val mockedAccessToken = "mocked_access_token"

// we logged in the user
sessionManager.onLoggedIn(accessToken = mockedAccessToken, userId = mockUserId)

val request = Request.Builder()
.url(mockWebServer.url("test"))
.get()
.build()

okHttpClient.newCall(request).enqueue(IgnoredCallback)

val receivedRequest = mockWebServer.takeRequest()

assertTrue(
"The Authorization token is not present!",
receivedRequest.headers.contains(
SessionInterceptor.HEADER_AUTHORIZATION to mockedAccessToken
)
)
}

@Test
fun `assert when logged out the authorization header is NOT set`() {
sessionManager.onLoggedOut(null)

val request = Request.Builder()
.url(mockWebServer.url("test"))
.get()
.build()

okHttpClient.newCall(request).enqueue(IgnoredCallback)

val receivedRequest = mockWebServer.takeRequest()

assertFalse(
"The authorization token should not be set",
receivedRequest.headers.any { it.first == SessionInterceptor.HEADER_AUTHORIZATION }
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.tapptitude.network.fake

import com.tapptitude.session.SessionManager
import com.tapptitude.session.model.LoggedIn
import com.tapptitude.session.model.LoggedOut
import com.tapptitude.session.model.LoginState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

internal class FakeSessionManager : SessionManager {
private val _currentLoginStateFlow: MutableStateFlow<LoginState> = MutableStateFlow(
LoggedOut("")
)

override val currentLoginStateFlow: StateFlow<LoginState>
get() = _currentLoginStateFlow

override fun onLoggedIn(accessToken: String, userId: String) {
_currentLoginStateFlow.value = LoggedIn(
sessionToken = accessToken,
userId = userId
)
}

override fun onLoggedOut(lastUserId: String?) {
_currentLoginStateFlow.value = LoggedOut(
null
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.tapptitude.network.fake

import java.io.IOException
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response

/**
* An empty [Callback] to be used in testing.
* Does nothing.
*/
internal object IgnoredCallback : Callback {
override fun onFailure(call: Call, e: IOException) = Unit
override fun onResponse(call: Call, response: Response) = Unit
}