Browse Source

Merge pull request #2 from MrOzOn/unit_tests

homework for module Testing
MrOzOn 5 năm trước cách đây
mục cha
commit
306bbec636
21 tập tin đã thay đổi với 748 bổ sung82 xóa
  1. 2 1
      .gitignore
  2. 1 0
      build.gradle
  3. 10 0
      core_api/src/main/java/com/mrozon/core_api/providers/CoroutineContextProvider.kt
  4. 27 12
      feature_auth/build.gradle
  5. 0 24
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/ExampleInstrumentedTest.kt
  6. 17 0
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/MockTestRunner.kt
  7. 23 0
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/TestApp.kt
  8. 39 0
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/AppComponent.kt
  9. 30 0
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/FacadeComponent.kt
  10. 30 0
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/NavigationModule.kt
  11. 16 0
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/SplashNavigatorImpl.kt
  12. 26 0
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/TestNavigatorImpl.kt
  13. 85 0
      feature_auth/src/androidTest/java/com/mrozon/feature_auth/presentation/RegistrationFragmentTest.kt
  14. 0 7
      feature_auth/src/main/java/com/mrozon/feature_auth/data/UserAuthRemoteDataSource.kt
  15. 2 0
      feature_auth/src/main/java/com/mrozon/feature_auth/data/UserAuthRepositoryImpl.kt
  16. 6 3
      feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragmentViewModel.kt
  17. 0 17
      feature_auth/src/test/java/com/mrozon/feature_auth/ExampleUnitTest.kt
  18. 204 0
      feature_auth/src/test/java/com/mrozon/feature_auth/data/UserAuthRepositoryImplTest.kt
  19. 160 0
      feature_auth/src/test/java/com/mrozon/feature_auth/presentation/RegistrationFragmentViewModelTest.kt
  20. 41 0
      jacoco.gradle
  21. 29 18
      scripts/deps_versions.gradle

+ 2 - 1
.gitignore

@@ -83,4 +83,5 @@ lint/generated/
 lint/outputs/
 lint/tmp/
 # lint/reports/
-/.idea/
+/.idea/
+/projectFilesBackup/.idea/workspace.xml

+ 1 - 0
build.gradle

@@ -17,6 +17,7 @@ buildscript {
         classpath 'com.android.tools.build:gradle:3.6.4'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
         classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.10.0"
+        classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4'
     }
 }
 

+ 10 - 0
core_api/src/main/java/com/mrozon/core_api/providers/CoroutineContextProvider.kt

@@ -0,0 +1,10 @@
+package com.mrozon.core_api.providers
+
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+
+open class CoroutineContextProvider @Inject constructor(){
+    open val Main: CoroutineContext by lazy { Dispatchers.Main }
+    open val IO: CoroutineContext by lazy { Dispatchers.IO }
+}

+ 27 - 12
feature_auth/build.gradle

@@ -3,6 +3,8 @@ apply plugin: 'kotlin-android'
 apply plugin: 'kotlin-android-extensions'
 apply plugin: 'kotlin-kapt'
 
+apply from: "$project.rootDir/jacoco.gradle"
+
 android {
     compileSdkVersion rootProject.compileSdkVersion
     buildToolsVersion "29.0.3"
@@ -13,11 +15,14 @@ android {
         versionCode 1
         versionName "1.0"
 
-        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        testInstrumentationRunner "com.mrozon.feature_auth.MockTestRunner"//"androidx.test.runner.AndroidJUnitRunner"
         consumerProguardFiles "consumer-rules.pro"
     }
 
     buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
         release {
             minifyEnabled false
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
@@ -38,25 +43,35 @@ android {
 apply from: "$project.rootDir/scripts/deps_versions.gradle"
 
 dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation kotlinStdlib
+
     api project(':core_api')
     implementation project(':core')
+    implementation project(':utils')
     //DAGGER
     implementation dagger
-    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
     kapt daggerCompiler
-    implementation project(':utils')
     implementation constraintlayout
     //Timber
     implementation timber
     implementation navigationFragment
     implementation retrofit
-
-    implementation fileTree(dir: "libs", include: ["*.jar"])
-    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
-    implementation 'androidx.core:core-ktx:1.3.1'
-    implementation 'androidx.appcompat:appcompat:1.2.0'
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
-
+    //AndroidX
+    implementation legacySupport
+    implementation appCompat
+    implementation androidxCore
+    //Unit test
+    testImplementation junit
+    testImplementation mockitoCore
+    testImplementation kotlinxCoroutinesTest
+    testImplementation robolectric
+    testImplementation androidXCoreTest
+    //Instrumental Test
+    androidTestImplementation junitInstrumental
+    androidTestImplementation androidXRulesTest
+    androidTestImplementation androidXRunnerTest
+    androidTestImplementation espressoCore
+    androidTestImplementation androidXFragmentTest
+    kaptAndroidTest daggerCompiler
 }

+ 0 - 24
feature_auth/src/androidTest/java/com/mrozon/feature_auth/ExampleInstrumentedTest.kt

@@ -1,24 +0,0 @@
-package com.mrozon.feature_auth
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
-    @Test
-    fun useAppContext() {
-        // Context of the app under test.
-        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
-        assertEquals("com.mrozon.feature_auth.test", appContext.packageName)
-    }
-}

+ 17 - 0
feature_auth/src/androidTest/java/com/mrozon/feature_auth/MockTestRunner.kt

@@ -0,0 +1,17 @@
+package com.mrozon.feature_auth
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import com.mrozon.feature_auth.presentation.TestApp
+
+class MockTestRunner: AndroidJUnitRunner() {
+
+    override fun newApplication(
+        cl: ClassLoader?,
+        className: String?,
+        context: Context?
+    ): Application {
+        return super.newApplication(cl, TestApp::class.java.name, context)
+    }
+}

+ 23 - 0
feature_auth/src/androidTest/java/com/mrozon/feature_auth/TestApp.kt

@@ -0,0 +1,23 @@
+package com.mrozon.feature_auth.presentation
+
+import android.app.Application
+import com.mrozon.core_api.providers.AppWithFacade
+import com.mrozon.core_api.providers.ProvidersFacade
+import com.mrozon.feature_auth.di.FacadeComponent
+
+class TestApp: Application(), AppWithFacade {
+
+    companion object {
+        private var facadeComponent: FacadeComponent? = null
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        (getFacade() as FacadeComponent).inject(this)
+    }
+
+    override fun getFacade(): ProvidersFacade {
+        return facadeComponent ?: FacadeComponent.init(this).also { facadeComponent = it }
+    }
+
+}

+ 39 - 0
feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/AppComponent.kt

@@ -0,0 +1,39 @@
+package com.mrozon.feature_auth.di
+
+import android.app.Application
+import android.content.Context
+import com.mrozon.core_api.providers.AppProvider
+import dagger.BindsInstance
+import dagger.Component
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+    modules = []
+)
+interface AppComponent: AppProvider {
+
+    companion object {
+
+        private var appComponent: AppProvider? = null
+
+        fun create(application: Application): AppProvider {
+            return appComponent ?: DaggerAppComponent
+                .builder()
+                .application(application.applicationContext)
+                .build().also {
+                    appComponent = it
+                }
+        }
+    }
+
+    @Component.Builder
+    interface Builder {
+
+        @BindsInstance
+        fun application(context: Context): Builder
+
+        fun build(): AppComponent
+    }
+
+}

+ 30 - 0
feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/FacadeComponent.kt

@@ -0,0 +1,30 @@
+package com.mrozon.feature_auth.di
+
+import android.app.Application
+import com.mrozon.core.CoreProvidersFactory
+import com.mrozon.core_api.db.DatabaseProvider
+import com.mrozon.core_api.network.NetworkProvider
+import com.mrozon.core_api.providers.AppProvider
+import com.mrozon.core_api.providers.ProvidersFacade
+import com.mrozon.core_api.viewmodel.ViewModelsFactoryProvider
+import com.mrozon.feature_auth.presentation.TestApp
+import dagger.Component
+
+@Component(
+    dependencies = [AppProvider::class, DatabaseProvider::class, NetworkProvider::class],
+    modules = [NavigationModule::class]
+)
+interface FacadeComponent : ProvidersFacade {
+
+    companion object {
+
+        fun init(application: Application): FacadeComponent =
+            DaggerFacadeComponent.builder()
+                .appProvider(AppComponent.create(application))
+                .databaseProvider(CoreProvidersFactory.createDatabaseBuilder(AppComponent.create(application)))
+                .networkProvider(CoreProvidersFactory.createNetworkBuilder())
+                .build()
+    }
+
+    fun inject(app: TestApp)
+}

+ 30 - 0
feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/NavigationModule.kt

@@ -0,0 +1,30 @@
+package com.mrozon.feature_auth.di
+
+import com.mrozon.core_api.navigation.*
+import dagger.Binds
+import dagger.Module
+import dagger.Reusable
+
+@Module
+interface NavigationModule {
+
+    @Reusable
+    @Binds
+    fun splashNavigator(navigator: SplashNavigatorImpl): SplashNavigator
+
+    @Reusable
+    @Binds
+    fun loginNavigator(navigator: TestNavigatorImpl): LoginNavigator
+
+    @Reusable
+    @Binds
+    fun registrationNavigator(navigator: TestNavigatorImpl): RegistrationNavigator
+
+    @Reusable
+    @Binds
+    fun listPersonNavigator(navigator: TestNavigatorImpl): ListPersonNavigator
+
+    @Reusable
+    @Binds
+    fun editPersonNavigator(navigator: TestNavigatorImpl): EditPersonNavigator
+}

+ 16 - 0
feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/SplashNavigatorImpl.kt

@@ -0,0 +1,16 @@
+package com.mrozon.feature_auth.di
+
+import androidx.navigation.NavController
+import com.mrozon.core_api.navigation.SplashNavigator
+import javax.inject.Inject
+
+class SplashNavigatorImpl @Inject constructor()
+    :SplashNavigator {
+
+    override fun navigateToAuth(navController: NavController) {
+    }
+
+    override fun navigateToListPerson(navController: NavController) {
+    }
+
+}

+ 26 - 0
feature_auth/src/androidTest/java/com/mrozon/feature_auth/di/TestNavigatorImpl.kt

@@ -0,0 +1,26 @@
+package com.mrozon.feature_auth.di
+
+import androidx.navigation.NavController
+import com.mrozon.core_api.navigation.EditPersonNavigator
+import com.mrozon.core_api.navigation.ListPersonNavigator
+import com.mrozon.core_api.navigation.LoginNavigator
+import com.mrozon.core_api.navigation.RegistrationNavigator
+import javax.inject.Inject
+
+class TestNavigatorImpl @Inject constructor(): RegistrationNavigator, LoginNavigator,
+    ListPersonNavigator, EditPersonNavigator {
+
+    override fun navigateToLoginUser(navController: NavController, userName: String) {
+
+    }
+
+    override fun navigateToRegisterUser(navController: NavController) {
+    }
+
+    override fun navigateToListPerson(navController: NavController) {
+    }
+
+    override fun navigateToEditPerson(navController: NavController, title: String, id: Long) {
+
+    }
+}

+ 85 - 0
feature_auth/src/androidTest/java/com/mrozon/feature_auth/presentation/RegistrationFragmentTest.kt

@@ -0,0 +1,85 @@
+package com.mrozon.feature_auth.presentation
+
+import android.content.Context
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.*
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.mrozon.feature_auth.R
+import org.hamcrest.Matchers.not
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RegistrationFragmentTest {
+
+    lateinit var context: Context
+
+    @Before
+    fun setup() {
+        context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+    }
+
+    @Test
+    fun all_input_value_is_corrected() {
+
+        launchFragmentInContainer<RegistrationFragment>()
+
+        val password = "Password1!"
+        onView(withId(R.id.etUserName)).perform(ViewActions.typeText("vasya@mail.ru"))
+        onView(withId(R.id.etUserPassword)).perform(ViewActions.typeText(password))
+        onView(withId(R.id.etUserPasswordAgain)).perform(ViewActions.typeText(password))
+        onView(withId(R.id.etFirstName)).perform(ViewActions.typeText("first_name"))
+        onView(withId(R.id.etLastName)).perform(ViewActions.typeText("last_name"))
+
+        onView(withId(R.id.btnRegistration)).check(matches(isEnabled()))
+    }
+
+    @Test
+    fun input_incorrect_email() {
+
+        launchFragmentInContainer<RegistrationFragment>()
+
+        val password = "Password1!"
+        onView(withId(R.id.etUserName)).perform(ViewActions.typeText("vasyamail.ru"))
+        onView(withId(R.id.etUserPassword)).perform(ViewActions.typeText(password))
+        onView(withId(R.id.etUserPasswordAgain)).perform(ViewActions.typeText(password))
+        onView(withId(R.id.etFirstName)).perform(ViewActions.typeText("first_name"))
+        onView(withId(R.id.etLastName)).perform(ViewActions.typeText("last_name"))
+
+        onView(withId(R.id.etUserName)).check(matches(hasErrorText(context.getString(R.string.uncorrect_email))))
+    }
+
+    @Test
+    fun input_different_passwords() {
+
+        launchFragmentInContainer<RegistrationFragment>()
+
+        onView(withId(R.id.etUserName)).perform(ViewActions.typeText("vasya@mail.ru"))
+        onView(withId(R.id.etUserPassword)).perform(ViewActions.typeText("password"))
+        onView(withId(R.id.etUserPasswordAgain)).perform(ViewActions.typeText("1"))
+        onView(withId(R.id.etFirstName)).perform(ViewActions.typeText("first_name"))
+        onView(withId(R.id.etLastName)).perform(ViewActions.typeText("last_name"))
+
+        onView(withId(R.id.etUserPassword)).check(matches(hasErrorText(context.getString(R.string.equals_psw))))
+    }
+
+    @Test
+    fun dont_type_all_needed_params() {
+
+        launchFragmentInContainer<RegistrationFragment>()
+
+        val password = "Password1!"
+        onView(withId(R.id.etUserName)).perform(ViewActions.typeText("vasya@mail.ru"))
+        onView(withId(R.id.etUserPassword)).perform(ViewActions.typeText(password))
+        onView(withId(R.id.etUserPasswordAgain)).perform(ViewActions.typeText(password))
+        onView(withId(R.id.etFirstName)).perform(ViewActions.typeText("first_name"))
+
+        onView(withId(R.id.btnRegistration)).check(matches(not(isEnabled())))
+    }
+
+}

+ 0 - 7
feature_auth/src/main/java/com/mrozon/feature_auth/data/UserAuthRemoteDataSource.kt

@@ -8,16 +8,9 @@ import javax.inject.Inject
 
 class UserAuthRemoteDataSource @Inject constructor(private val service: HealthDiaryService): BaseDataSource() {
 
-    suspend fun loginUser(userName: String, userPsw: String)
-            = getResult { service.loginUser(LoginRequest(username = userName, password = userPsw)) }
-
     suspend fun loginUser(loginRequest: LoginRequest)
             = getResult { service.loginUser(loginRequest) }
 
-    suspend fun registerUser(email: String, password: String, firstName: String, lastName: String)
-            = getResult { service.registerUser(RegisterRequest(email = email, username = email,
-                password = password, first_name = firstName, last_name = lastName )) }
-
     suspend fun registerUser(registerRequest: RegisterRequest)
             = getResult { service.registerUser(registerRequest) }
 

+ 2 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/data/UserAuthRepositoryImpl.kt

@@ -12,6 +12,7 @@ import com.mrozon.utils.network.Result
 import com.mrozon.utils.network.Result.Companion.error
 import com.mrozon.utils.network.Result.Companion.loading
 import com.mrozon.utils.network.Result.Companion.success
+import timber.log.Timber
 
 @Singleton
 class UserAuthRepositoryImpl @Inject constructor(private val userAuthRemoteDataSource: UserAuthRemoteDataSource,
@@ -39,6 +40,7 @@ class UserAuthRepositoryImpl @Inject constructor(private val userAuthRemoteDataS
             val request = LoginRequest(username = userName, password = password)
             val response = userAuthRemoteDataSource.loginUser(request)
             if (response.status == Result.Status.SUCCESS) {
+                Timber.d("sahdksdh")
                 dao.insertUser(response.data!!.toUserDb())
                 emit(success(response.data!!.toUser()))
             } else if (response.status == Result.Status.ERROR) {

+ 6 - 3
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragmentViewModel.kt

@@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
 import com.mrozon.core_api.entity.User
+import com.mrozon.core_api.providers.CoroutineContextProvider
 import com.mrozon.feature_auth.data.UserAuthRepository
 import com.mrozon.feature_auth.data.UserAuthRepositoryImpl
 import com.mrozon.utils.base.BaseViewModel
@@ -16,9 +17,11 @@ import kotlinx.coroutines.flow.asFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
 
 class RegistrationFragmentViewModel @Inject constructor(
-    private val repository: UserAuthRepository
+    private val repository: UserAuthRepository,
+    private val coroutineContextProvider: CoroutineContextProvider
 ): BaseViewModel() {
 
     private val _error = MutableLiveData<String?>()
@@ -88,9 +91,9 @@ class RegistrationFragmentViewModel @Inject constructor(
     fun registerUser() {
         val psw = passwordChannel.value
         val user = User(email = emailChannel.value, firstname = firstNameChannel.value, lastname = lastNameChannel.value)
-        viewModelScope.launch(Dispatchers.IO) {
+        viewModelScope.launch(coroutineContextProvider.IO) {
             repository.registerUser(user,psw).collect {
-                withContext(Dispatchers.Main) {
+                withContext(coroutineContextProvider.Main) {
                     _registeredUser.value = it
                 }
             }

+ 0 - 17
feature_auth/src/test/java/com/mrozon/feature_auth/ExampleUnitTest.kt

@@ -1,17 +0,0 @@
-package com.mrozon.feature_auth
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
-    @Test
-    fun addition_isCorrect() {
-        assertEquals(4, 2 + 2)
-    }
-}

+ 204 - 0
feature_auth/src/test/java/com/mrozon/feature_auth/data/UserAuthRepositoryImplTest.kt

@@ -0,0 +1,204 @@
+package com.mrozon.feature_auth.data
+
+import com.mrozon.core_api.db.HealthDiaryDao
+import com.mrozon.core_api.network.HealthDiaryService
+import com.mrozon.core_api.network.model.*
+import com.mrozon.utils.network.Result.Companion.loading
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import org.junit.Assert.*
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import com.mrozon.utils.network.Result.Companion.error
+import com.mrozon.utils.network.Result.Companion.success
+import kotlinx.coroutines.test.*
+import okhttp3.MediaType
+import okhttp3.ResponseBody
+import org.junit.Before
+import org.junit.runners.model.Statement
+import org.mockito.Mock
+import org.mockito.Mockito.*
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import retrofit2.Response
+
+@ExperimentalCoroutinesApi
+class UserAuthRepositoryImplTest {
+
+    @get:Rule
+    val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    lateinit var apiService: HealthDiaryService
+
+    @Mock
+    lateinit var dao: HealthDiaryDao
+
+    lateinit var repository: UserAuthRepository
+
+    @get:Rule
+    var coroutinesTestRule = CoroutineTestRule()
+
+    @Before
+    fun setUp() {
+        val source = UserAuthRemoteDataSource(apiService)
+        repository = UserAuthRepositoryImpl(source, dao)
+    }
+
+    @Test
+    fun `register user success`() = coroutinesTestRule.testDispatcher.runBlockingTest {
+        val email = "test@mail.ru"
+        val password = "password1"
+        val request = RegisterRequest(email = email, first_name = "",
+            last_name = "",username = email, password = password)
+        val response = RegisterResponse(email=email, id = 1, first_name =  "",
+            last_name = "", last_login = "", username = email )
+        val expected = listOf(
+            loading(),
+            success(response.toUser())
+        )
+
+        `when`(apiService.registerUser(request)).thenReturn(
+            Response.success(response)
+        )
+
+        val actual = repository.registerUser(response.toUser(),password)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `register user failed`() = coroutinesTestRule.testDispatcher.runBlockingTest {
+        val error = "Bla-Bla!"
+        val email = "test@mail.ru"
+        val password = "password1"
+        val request = RegisterRequest(email = email, first_name = "",
+            last_name = "",username = email, password = password)
+        val response = RegisterResponse(email=email, id = 1, first_name =  "",
+            last_name = "", last_login = "", username = email )
+        val expected = listOf(
+            loading(),
+            error("Network error:\n $error",null)
+        )
+
+        `when`(apiService.registerUser(request)).thenReturn(
+            Response.error(402, ResponseBody.create(MediaType.get("text/plain"),"[$error]"))
+        )
+
+        val actual = repository.registerUser(response.toUser(),password)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `login user success`() = coroutinesTestRule.testDispatcher.runBlockingTest {
+
+        val username = "vasya@mail.ru"
+        val password = "strong_password"
+        val request = LoginRequest(username,password)
+        val response = LoginResponse(email = username, first_name = "",
+            last_name = "", token = "aaaaa", user_id = 101)
+
+        val expected = listOf(
+            loading(),
+            success(response.toUser())
+        )
+
+        `when`(apiService.loginUser(request)).thenReturn(
+            Response.success(response)
+        )
+
+        val actual = repository.loginUser(username,password)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+    @Test
+    fun `logged user added in db`() = coroutinesTestRule.testDispatcher.runBlockingTest {
+
+        val username = "vasya@mail.ru"
+        val password = "strong_password"
+        val request = LoginRequest(username,password)
+        val response = LoginResponse(email = username, first_name = "first_name",
+            last_name = "last_name", token = "aaaaa", user_id = 101)
+
+        `when`(apiService.loginUser(request)).thenReturn(
+            Response.success(response)
+        )
+
+        repository.loginUser(username,password).toList()
+
+        verify(dao).insertUser(response.toUserDb())
+    }
+
+    @Test
+    fun `login user failed`() = coroutinesTestRule.testDispatcher.runBlockingTest {
+        val error = "Bla-Bla!"
+        val username = "vasya@mail.ru"
+        val password = "strong_password"
+        val request = LoginRequest(username,password)
+
+        val expected = listOf(
+            loading(),
+            error("Network error:\n $error",null)
+        )
+
+        `when`(apiService.loginUser(request)).thenReturn(
+            Response.error(402, ResponseBody.create(MediaType.get("text/plain"),"[$error]"))
+        )
+
+        val actual = repository.loginUser(username,password)
+
+        assertEquals(
+            expected,
+            actual.toList()
+        )
+    }
+
+
+
+}
+
+@ExperimentalCoroutinesApi
+class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {
+
+    private val testCoroutinesScope = TestCoroutineScope(testDispatcher)
+
+//    override fun starting(description: Description?) {
+//        super.starting(description)
+//        Dispatchers.setMain(testDispatcher)
+//    }
+//
+//    override fun finished(description: Description?) {
+//        super.finished(description)
+//        Dispatchers.resetMain()
+//        testDispatcher.cleanupTestCoroutines()
+//    }
+
+    override fun apply(base: Statement, description: Description?) = object : Statement() {
+        override fun evaluate() {
+            Dispatchers.setMain(testDispatcher)
+            base.evaluate()
+            Dispatchers.resetMain()
+            testCoroutinesScope.cleanupTestCoroutines()
+        }
+    }
+
+    fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
+        testCoroutinesScope.runBlockingTest{
+            block()
+        }
+    }
+}

+ 160 - 0
feature_auth/src/test/java/com/mrozon/feature_auth/presentation/RegistrationFragmentViewModelTest.kt

@@ -0,0 +1,160 @@
+package com.mrozon.feature_auth.presentation
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.Observer
+import com.mrozon.core_api.entity.User
+import com.mrozon.core_api.providers.CoroutineContextProvider
+import com.mrozon.feature_auth.data.CoroutineTestRule
+import com.mrozon.feature_auth.data.UserAuthRepository
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.flowOf
+import org.junit.*
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import kotlin.coroutines.CoroutineContext
+
+
+@ExperimentalCoroutinesApi
+@RunWith(RobolectricTestRunner::class)
+@Config(manifest= Config.NONE)
+class RegistrationFragmentViewModelTest {
+
+    @get:Rule
+    val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @get:Rule
+    var coroutinesTestRule = CoroutineTestRule()
+
+    @get:Rule
+    val executorRule = InstantTaskExecutorRule()
+
+    @Mock
+    lateinit var repository: UserAuthRepository
+
+    @Mock
+    lateinit var observer: Observer<Result<User>?>
+
+    @Mock
+    lateinit var validateObserver: Observer<ValidateDataError>
+
+    lateinit var viewModel: RegistrationFragmentViewModel
+
+    @Before
+    fun setUp() {
+        viewModel = RegistrationFragmentViewModel(repository, TestCoroutineProvider())
+            .apply {
+                registeredUser.observeForever(observer)
+            }
+    }
+
+    @After
+    fun tearDown() {
+    }
+
+    @Test
+    fun `register user success`() = coroutinesTestRule.runBlockingTest {
+        val password = "password1"
+        val email = "vasya@mail.ru"
+        val firstName = "first"
+        val lastName = "last"
+        viewModel.passwordChannel.offer(password)
+        viewModel.emailChannel.offer(email)
+        viewModel.firstNameChannel.offer(firstName)
+        viewModel.lastNameChannel.offer(lastName)
+        val user = User(email = email, firstname = firstName, lastname = lastName)
+
+        Mockito.`when`(repository.registerUser(Mockito.any<User>()?:user, Mockito.anyString())).thenReturn(
+            flowOf(Result.loading(), Result.success(user))
+        )
+
+        viewModel.registerUser()
+
+        Mockito.verify(observer).onChanged(Result.loading())
+        Mockito.verify(observer).onChanged(Result.success(user))
+    }
+
+    @Test
+    fun `register user failed`() = coroutinesTestRule.runBlockingTest {
+        val password = "password1"
+        val email = "vasya@mail.ru"
+        val firstName = "first"
+        val lastName = "last"
+        val error = "Bla-bla!"
+        viewModel.passwordChannel.offer(password)
+        viewModel.emailChannel.offer(email)
+        viewModel.firstNameChannel.offer(firstName)
+        viewModel.lastNameChannel.offer(lastName)
+        val user = User(email = email, firstname = firstName, lastname = lastName)
+
+        Mockito.`when`(repository.registerUser(Mockito.any<User>()?:user, Mockito.anyString())).thenReturn(
+            flowOf(Result.loading(), Result.error(error))
+        )
+
+        viewModel.registerUser()
+
+        Mockito.verify(observer).onChanged(Result.loading())
+        Mockito.verify(observer).onChanged(Result.error(error))
+    }
+
+    @FlowPreview
+    @Test
+    fun `different password typed is failed`()  {
+
+        viewModel.validateData.observeForever(validateObserver)
+        viewModel.passwordChannel.offer("111")
+        viewModel.passwordConfirmChannel.offer("1111")
+        viewModel.emailChannel.offer("vasya@mail.ru")
+        viewModel.firstNameChannel.offer("firstName")
+        viewModel.lastNameChannel.offer("lastName")
+
+        Mockito.verify(validateObserver).onChanged(ValidateDataError.PASSWORD_NOT_EQUAL)
+
+        viewModel.validateData.removeObserver(validateObserver)
+    }
+
+    @FlowPreview
+    @Test
+    fun `input values all correct`() {
+
+        val password = "password"
+        viewModel.validateData.observeForever(validateObserver)
+        viewModel.passwordChannel.offer(password)
+        viewModel.passwordConfirmChannel.offer(password)
+        viewModel.emailChannel.offer("vasya@mail.ru")
+        viewModel.firstNameChannel.offer("firstName")
+        viewModel.lastNameChannel.offer("lastName")
+
+        Mockito.verify(validateObserver).onChanged(ValidateDataError.OK)
+
+        viewModel.validateData.removeObserver(validateObserver)
+    }
+
+    @FlowPreview
+    @Test
+    fun `incorrect email is failed`() {
+
+        val password = "password"
+        viewModel.validateData.observeForever(validateObserver)
+        viewModel.passwordChannel.offer(password)
+        viewModel.passwordConfirmChannel.offer(password)
+        viewModel.emailChannel.offer("vasyamail.ru")
+        viewModel.firstNameChannel.offer("firstName")
+        viewModel.lastNameChannel.offer("lastName")
+
+        Mockito.verify(validateObserver).onChanged(ValidateDataError.INCORRECT_EMAIL)
+
+        viewModel.validateData.removeObserver(validateObserver)
+    }
+
+}
+
+class TestCoroutineProvider: CoroutineContextProvider() {
+    override val Main: CoroutineContext = Dispatchers.Unconfined
+    override val IO: CoroutineContext = Dispatchers.Unconfined
+}

+ 41 - 0
jacoco.gradle

@@ -0,0 +1,41 @@
+apply plugin: 'jacoco-android'
+
+jacoco {
+    toolVersion = "0.8.5"
+}
+
+tasks.withType(Test) {
+    jacoco.includeNoLocationClasses = true
+}
+
+jacocoAndroidUnitTestReport {
+    excludes += [
+            '**/R.class',
+            '**/R$*.class',
+            '**/BuildConfig.*',
+            '**/Manifest*.*',
+            '**/*Test*.*',
+            '**/*Fragment*.*',
+            '**/android/databinding/*',
+            '**/androidx/databinding/*',
+            '**/*Binding*.*',
+            '**/di/module/*',
+            '**/*MapperImpl*.*',
+            '**/*$ViewInjector*.*',
+            '**/*$ViewBinder*.*',
+            '**/BuildConfig.*',
+            '**/*Component*.*',
+            '**/*BR*.*',
+            '**/Manifest*.*',
+            '**/*$Lambda$*.*',
+            '**/*Companion*.*',
+            '**/*Module.*',
+            '**/*Dagger*.*',
+            '**/*MembersInjector*.*',
+            '**/*_Factory*.*',
+            '**/*_Provide*Factory*.*',
+            '**/*Extensions*.*',
+            '**/*$Result.*', /* filtering `sealed` and `data` classes */
+            '**/*$Result$*.*'/* filtering `sealed` and `data` classes */
+    ]
+}

+ 29 - 18
scripts/deps_versions.gradle

@@ -1,14 +1,8 @@
 ext {
 
 
-//    jsr330Version = '1'
     daggerVersion = '2.25.2'
     roomVersion = '2.1.0'
-//    picassoVersion = '2.71828'
-//    flexLayoutVersion = '1.1.1'
-//    viewPager2Version = '1.0.0-rc01'
-//    coroutinesVersion = '1.1.1'
-//    materialVersion = '1.1.0-beta02'
     navigationVersion = '2.3.0'
     timberVersion = '4.7.1'
     retrofitVersion = '2.6.1'
@@ -19,9 +13,18 @@ ext {
     recyclerviewViersion = '1.2.0-alpha05'
     materialVersion = '1.2.1'
     lifecycleVersion = '2.3.0-alpha07'
+    junitVersion = "4.12"
+    mockitoCoreVersion = "2.28.2"
+    kotlinxCoroutinesTestVersion = "1.3.1"
+    junitInstrumentalVersion = "1.1.1"
+    espressoCoreVersion = "3.2.0"
+    legacySupportVersion = "1.0.0"
+    appCompatVersion = "1.2.0"
+    androidxCoreVersion = "1.3.1"
+    robolectricVersion = "4.4"
+    androidXCoreTestVersion = "2.1.0"
+    androidXTestVersion = "1.1.0"
 
-//    retrofit = "com.squareup.retrofit2:retrofit:$retrofitVersion"
-//    jsr330 = "javax.inject:javax.inject:$jsr330Version"
     // DI
     dagger = "com.google.dagger:dagger:$daggerVersion"
     daggerCompiler = "com.google.dagger:dagger-compiler:$daggerVersion"
@@ -29,16 +32,6 @@ ext {
     room = "androidx.room:room-runtime:$roomVersion"
     roomKtx = "androidx.room:room-ktx:$roomVersion"
     roomCompiler = "androidx.room:room-compiler:$roomVersion"
-//    picasso = "com.squareup.picasso:picasso:$picassoVersion"
-//    flexLayout = "com.google.android:flexbox:$flexLayoutVersion"
-//    viewPager2 = "androidx.viewpager2:viewpager2:$viewPager2Version"
-//    coroutinesDependencies = [
-//            'coroutines_core'   : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion",
-//            'coroutines_android': "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
-//    ]
-//
-//    material = "com.google.android.material:material:$materialVersion"
-
     // Navigation
     navigationFragment = "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
     navigationUi = "androidx.navigation:navigation-ui-ktx:$navigationVersion"
@@ -61,4 +54,22 @@ ext {
     lifecycleExtensions = "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
     lifecycleLivedata = "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
     lifecycleViewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
+    // Testing
+    junit = "junit:junit:$junitVersion"
+    mockitoCore = "org.mockito:mockito-core:$mockitoCoreVersion"
+    kotlinxCoroutinesTest =  "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesTestVersion"
+    junitInstrumental =  "androidx.test.ext:junit:$junitInstrumentalVersion"
+    espressoCore = "androidx.test.espresso:espresso-core:$espressoCoreVersion"
+    androidXRunnerTest = "androidx.test:runner:$androidXTestVersion"
+    androidXRulesTest = "androidx.test:rules:$androidXTestVersion"
+    robolectric = "org.robolectric:robolectric:$robolectricVersion"
+    androidXCoreTest = "androidx.arch.core:core-testing:$androidXCoreTestVersion"
+    androidXFragmentTest = "androidx.fragment:fragment-testing:$androidXTestVersion"
+    // Support Library AndroidX
+    legacySupport = "androidx.legacy:legacy-support-v4:$legacySupportVersion"
+    appCompat =  "androidx.appcompat:appcompat:$appCompatVersion"
+    androidxCore = "androidx.core:core-ktx:$androidxCoreVersion"
+    // Kotlin
+    kotlinStdlib = "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
 }