Skip to content

Commit 8b06b7f

Browse files
Merge pull request #57 from rootstrap/unit_test
Unit test examples with coroutines
2 parents e5642b4 + 32ba4da commit 8b06b7f

File tree

11 files changed

+255
-8
lines changed

11 files changed

+255
-8
lines changed

app/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,10 @@ android {
141141
dependencies {
142142
def room_version = "2.3.0"
143143
def lifecycle_version = "2.4.0"
144+
def mockkVersion = '1.12.0'
144145

145146
implementation fileTree(include: ['*.jar'], dir: 'libs')
146-
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
147+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
147148
implementation 'androidx.appcompat:appcompat:1.3.1'
148149
implementation 'androidx.core:core-ktx:1.7.0'
149150
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
@@ -152,6 +153,10 @@ dependencies {
152153
implementation 'com.google.android.material:material:1.4.0'
153154
testImplementation 'junit:junit:4.13.1'
154155
testImplementation 'org.mockito:mockito-core:2.28.2'
156+
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1"
157+
testImplementation 'android.arch.core:core-testing:1.1.1'
158+
testImplementation "io.mockk:mockk:$mockkVersion"
159+
155160
androidTestImplementation 'androidx.test:runner:1.4.0'
156161
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
157162
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'

app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivityViewModel.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.rootstrap.android.network.managers.user.UserManager
88
import com.rootstrap.android.network.models.User
99
import com.rootstrap.android.ui.base.BaseViewModel
1010
import com.rootstrap.android.util.NetworkState
11+
import com.rootstrap.android.util.dispatcher.DispatcherProvider
1112
import com.rootstrap.android.util.extensions.ApiErrorType
1213
import com.rootstrap.android.util.extensions.ApiException
1314
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -17,7 +18,8 @@ import javax.inject.Inject
1718
@HiltViewModel
1819
open class SignInActivityViewModel @Inject constructor(
1920
private val sessionManager: SessionManager,
20-
private val userManager: UserManager
21+
private val userManager: UserManager,
22+
private val dispatcher: DispatcherProvider
2123
) : BaseViewModel() {
2224

2325
private val _state = MutableLiveData<SignInState>()
@@ -26,7 +28,8 @@ open class SignInActivityViewModel @Inject constructor(
2628

2729
fun signIn(user: User) {
2830
_networkState.value = NetworkState.loading
29-
viewModelScope.launch {
31+
// Avoid using hardcoded dispatcher this way can be mocked later
32+
viewModelScope.launch(dispatcher.io) {
3033
val result = userManager.signIn(user = user)
3134
if (result.isSuccess) {
3235
result.getOrNull()?.value?.user?.let { user ->
@@ -46,7 +49,6 @@ open class SignInActivityViewModel @Inject constructor(
4649
exception.message
4750
} else null
4851

49-
_networkState.value = NetworkState.idle
5052
_networkState.value = NetworkState.error
5153
_state.value = SignInState.signInFailure
5254
}

app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivityViewModel.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.rootstrap.android.network.managers.user.UserManager
88
import com.rootstrap.android.network.models.User
99
import com.rootstrap.android.ui.base.BaseViewModel
1010
import com.rootstrap.android.util.NetworkState
11+
import com.rootstrap.android.util.dispatcher.DispatcherProvider
1112
import com.rootstrap.android.util.extensions.ApiErrorType
1213
import com.rootstrap.android.util.extensions.ApiException
1314
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -17,7 +18,8 @@ import javax.inject.Inject
1718
@HiltViewModel
1819
open class SignUpActivityViewModel @Inject constructor(
1920
private val sessionManager: SessionManager,
20-
private val userManager: UserManager
21+
private val userManager: UserManager,
22+
private val dispatcher: DispatcherProvider
2123
) : BaseViewModel() {
2224

2325
private val _state = MutableLiveData<SignUpState>()
@@ -26,7 +28,8 @@ open class SignUpActivityViewModel @Inject constructor(
2628

2729
fun signUp(user: User) {
2830
_networkState.value = NetworkState.loading
29-
viewModelScope.launch {
31+
// Avoid using hardcoded dispatcher this way can be mocked later
32+
viewModelScope.launch(dispatcher.io) {
3033
val result = userManager.signUp(user = user)
3134

3235
if (result.isSuccess) {
@@ -47,7 +50,6 @@ open class SignUpActivityViewModel @Inject constructor(
4750
exception.message
4851
} else null
4952

50-
_networkState.value = NetworkState.idle
5153
_networkState.value = NetworkState.error
5254
_state.value = SignUpState.signUpFailure
5355
}

app/src/main/java/com/rootstrap/android/util/UtilModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import android.security.keystore.KeyProperties
77
import androidx.security.crypto.EncryptedSharedPreferences
88
import androidx.security.crypto.MasterKey
99
import com.rootstrap.android.BuildConfig
10+
import com.rootstrap.android.util.dispatcher.AppDispatcherProvider
11+
import com.rootstrap.android.util.dispatcher.DispatcherProvider
1012
import com.squareup.otto.Bus
1113
import dagger.Module
1214
import dagger.Provides
@@ -48,4 +50,8 @@ class UtilModule {
4850
@Provides
4951
@Singleton
5052
fun provideBus(): Bus = Bus()
53+
54+
@Provides
55+
@Singleton
56+
fun provideDispatcher(): DispatcherProvider = AppDispatcherProvider()
5157
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.rootstrap.android.util.dispatcher
2+
3+
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.Dispatchers
5+
6+
class AppDispatcherProvider : DispatcherProvider {
7+
override val io: CoroutineDispatcher = Dispatchers.IO
8+
override val default: CoroutineDispatcher = Dispatchers.Default
9+
override val main: CoroutineDispatcher = Dispatchers.Main
10+
override val unconfined: CoroutineDispatcher = Dispatchers.Unconfined
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.rootstrap.android.util.dispatcher
2+
3+
import kotlinx.coroutines.CoroutineDispatcher
4+
5+
interface DispatcherProvider {
6+
val io: CoroutineDispatcher
7+
val default: CoroutineDispatcher
8+
val main: CoroutineDispatcher
9+
val unconfined: CoroutineDispatcher
10+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.rootstrap.android
2+
3+
import com.rootstrap.android.network.managers.session.SessionManager
4+
import com.rootstrap.android.network.managers.user.UserManager
5+
import com.rootstrap.android.network.models.User
6+
import com.rootstrap.android.network.models.UserSerializer
7+
import com.rootstrap.android.test.TestDispatcherProvider
8+
import com.rootstrap.android.test.UnitTestBase
9+
import com.rootstrap.android.ui.activity.main.SignInActivityViewModel
10+
import com.rootstrap.android.ui.activity.main.SignInState
11+
import com.rootstrap.android.util.NetworkState
12+
import com.rootstrap.android.util.extensions.ApiException
13+
import com.rootstrap.android.util.extensions.Data
14+
import io.mockk.coEvery
15+
import io.mockk.coVerify
16+
import io.mockk.every
17+
import io.mockk.impl.annotations.MockK
18+
import io.mockk.impl.annotations.RelaxedMockK
19+
import io.mockk.verify
20+
import kotlinx.coroutines.ExperimentalCoroutinesApi
21+
import org.junit.Assert.assertEquals
22+
import org.junit.Before
23+
import org.junit.Test
24+
25+
@ExperimentalCoroutinesApi
26+
class SignInActivityViewModelTest : UnitTestBase() {
27+
28+
private lateinit var viewModel: SignInActivityViewModel
29+
30+
@RelaxedMockK
31+
lateinit var sessionManager: SessionManager
32+
33+
@RelaxedMockK
34+
lateinit var userManager: UserManager
35+
36+
@MockK
37+
lateinit var user: User
38+
39+
@MockK
40+
lateinit var userSerializer: UserSerializer
41+
42+
companion object {
43+
const val ERROR_EXAMPLE_TEXT = "Time out example"
44+
}
45+
46+
@Before
47+
override fun setup() {
48+
super.setup()
49+
every { userSerializer.user } returns user
50+
viewModel = SignInActivityViewModel(sessionManager, userManager, TestDispatcherProvider())
51+
}
52+
53+
// reading: naming standards for unit testing https://osherove.com/blog/2005/4/3/naming-standards-for-unit-tests.html
54+
@Test
55+
fun `signIn success assert signInSuccess and network idle`() {
56+
var state: SignInState? = null
57+
coEvery { userManager.signIn(user = user) } returns Result.success(Data(userSerializer))
58+
59+
viewModel.signIn(user)
60+
viewModel.state.observeForever {
61+
state = it
62+
}
63+
64+
assertEquals(state, SignInState.signInSuccess)
65+
assertEquals(viewModel.networkState.value, NetworkState.idle)
66+
verify { sessionManager.signIn(user) }
67+
coVerify { userManager.signIn(user = user) }
68+
}
69+
70+
@Test
71+
fun `signIn fail assert signInFailure and network error`() {
72+
var state: SignInState? = null
73+
coEvery { userManager.signIn(user = user) } returns Result.failure(
74+
ApiException(
75+
ERROR_EXAMPLE_TEXT
76+
)
77+
)
78+
79+
viewModel.signIn(user)
80+
viewModel.state.observeForever {
81+
state = it
82+
}
83+
84+
assertEquals(state, SignInState.signInFailure)
85+
assertEquals(viewModel.networkState.value, NetworkState.error)
86+
assertEquals(viewModel.error, ERROR_EXAMPLE_TEXT)
87+
coVerify { userManager.signIn(user = user) }
88+
}
89+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.rootstrap.android
2+
3+
import com.rootstrap.android.network.managers.session.SessionManager
4+
import com.rootstrap.android.network.managers.user.UserManager
5+
import com.rootstrap.android.network.models.User
6+
import com.rootstrap.android.network.models.UserSerializer
7+
import com.rootstrap.android.test.TestDispatcherProvider
8+
import com.rootstrap.android.test.UnitTestBase
9+
import com.rootstrap.android.ui.activity.main.SignUpActivityViewModel
10+
import com.rootstrap.android.ui.activity.main.SignUpState
11+
import com.rootstrap.android.util.NetworkState
12+
import com.rootstrap.android.util.extensions.ApiException
13+
import com.rootstrap.android.util.extensions.Data
14+
import io.mockk.coEvery
15+
import io.mockk.coVerify
16+
import io.mockk.every
17+
import io.mockk.impl.annotations.MockK
18+
import io.mockk.impl.annotations.RelaxedMockK
19+
import io.mockk.verify
20+
import kotlinx.coroutines.ExperimentalCoroutinesApi
21+
import org.junit.Assert.assertEquals
22+
import org.junit.Before
23+
import org.junit.Test
24+
25+
@ExperimentalCoroutinesApi // This annotation is required to use TestDispatcherProvider is still an experiment
26+
class SignUpActivityViewModelTest : UnitTestBase() {
27+
28+
private lateinit var viewModel: SignUpActivityViewModel
29+
30+
@RelaxedMockK
31+
lateinit var sessionManager: SessionManager
32+
33+
@RelaxedMockK
34+
lateinit var userManager: UserManager
35+
36+
@MockK
37+
lateinit var user: User
38+
39+
@MockK
40+
lateinit var userSerializer: UserSerializer
41+
42+
companion object {
43+
const val ERROR_EXAMPLE_TEXT = "Time out example"
44+
}
45+
46+
@Before
47+
override fun setup() {
48+
super.setup()
49+
every { userSerializer.user } returns user
50+
viewModel = SignUpActivityViewModel(sessionManager, userManager, TestDispatcherProvider())
51+
}
52+
53+
@Test
54+
fun `signUp success assert signUpSuccess and network idle`() {
55+
var state: SignUpState? = null
56+
coEvery { userManager.signUp(user = user) } returns Result.success(Data(userSerializer))
57+
58+
viewModel.signUp(user)
59+
viewModel.state.observeForever {
60+
state = it
61+
}
62+
63+
assertEquals(state, SignUpState.signUpSuccess)
64+
assertEquals(viewModel.networkState.value, NetworkState.idle)
65+
verify { sessionManager.signIn(user) }
66+
coVerify { userManager.signUp(user = user) }
67+
}
68+
69+
@Test
70+
fun `signUp fail assert signUpFailure and network error`() {
71+
var state: SignUpState? = null
72+
coEvery { userManager.signUp(user = user) } returns Result.failure(
73+
ApiException(
74+
ERROR_EXAMPLE_TEXT
75+
)
76+
)
77+
78+
viewModel.signUp(user)
79+
viewModel.state.observeForever {
80+
state = it
81+
}
82+
83+
assertEquals(state, SignUpState.signUpFailure)
84+
assertEquals(viewModel.networkState.value, NetworkState.error)
85+
assertEquals(viewModel.error, ERROR_EXAMPLE_TEXT)
86+
coVerify { userManager.signUp(user = user) }
87+
}
88+
}

app/src/test/java/com/rootstrap/android/ValidationTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import com.rootstrap.android.util.extensions.isEmail
44
import org.junit.Assert.assertEquals
55
import org.junit.Test
66

7-
public class ValidationTests {
7+
class ValidationTests {
88
@Test
99
fun checkEmailTest() {
1010
assertEquals(true, "email@mkdi.com".isEmail())
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.rootstrap.android.test
2+
3+
import com.rootstrap.android.util.dispatcher.DispatcherProvider
4+
import kotlinx.coroutines.CoroutineDispatcher
5+
import kotlinx.coroutines.ExperimentalCoroutinesApi
6+
import kotlinx.coroutines.test.TestCoroutineDispatcher
7+
8+
@ExperimentalCoroutinesApi
9+
class TestDispatcherProvider(testCoroutineDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) :
10+
DispatcherProvider {
11+
override val default: CoroutineDispatcher = testCoroutineDispatcher
12+
override val main: CoroutineDispatcher = testCoroutineDispatcher
13+
override val io: CoroutineDispatcher = testCoroutineDispatcher
14+
override val unconfined: CoroutineDispatcher = testCoroutineDispatcher
15+
}

0 commit comments

Comments
 (0)