From 40e57a27ac547b2993df0374b8d84aeb88b808cc Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 6 Feb 2020 11:45:53 -0300 Subject: [PATCH 1/3] create testing module --- albumlist/build.gradle | 2 ++ app/build.gradle | 2 ++ photolist/build.gradle | 2 ++ settings.gradle | 2 +- testing/.gitignore | 1 + testing/build.gradle | 6 ++++++ testing/consumer-rules.pro | 0 testing/proguard-rules.pro | 21 +++++++++++++++++++++ testing/src/main/AndroidManifest.xml | 2 ++ testing/src/main/res/values/strings.xml | 3 +++ userlist/build.gradle | 2 ++ 11 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 testing/.gitignore create mode 100644 testing/build.gradle create mode 100644 testing/consumer-rules.pro create mode 100644 testing/proguard-rules.pro create mode 100644 testing/src/main/AndroidManifest.xml create mode 100644 testing/src/main/res/values/strings.xml diff --git a/albumlist/build.gradle b/albumlist/build.gradle index d51b157..e59dcbe 100644 --- a/albumlist/build.gradle +++ b/albumlist/build.gradle @@ -3,4 +3,6 @@ apply from: '../core.gradle' dependencies { implementation project(':base') + testImplementation project(':testing') + androidTestImplementation project(':testing') } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index feaac15..3d4fc2f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,4 +51,6 @@ dependencies { implementation project(":userlist") implementation project(":albumlist") implementation project(":photolist") + testImplementation project(':testing') + androidTestImplementation project(':testing') } diff --git a/photolist/build.gradle b/photolist/build.gradle index d51b157..e59dcbe 100644 --- a/photolist/build.gradle +++ b/photolist/build.gradle @@ -3,4 +3,6 @@ apply from: '../core.gradle' dependencies { implementation project(':base') + testImplementation project(':testing') + androidTestImplementation project(':testing') } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 469cf91..e43c830 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':app', ':base', ':userlist', ':albumlist', ':photolist' +include ':app', ':base', ':userlist', ':albumlist', ':photolist', ':testing' rootProject.name='Friendlists' diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1 @@ +/build diff --git a/testing/build.gradle b/testing/build.gradle new file mode 100644 index 0000000..d51b157 --- /dev/null +++ b/testing/build.gradle @@ -0,0 +1,6 @@ +apply plugin: 'com.android.library' +apply from: '../core.gradle' + +dependencies { + implementation project(':base') +} \ No newline at end of file diff --git a/testing/consumer-rules.pro b/testing/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/testing/proguard-rules.pro b/testing/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/testing/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/testing/src/main/AndroidManifest.xml b/testing/src/main/AndroidManifest.xml new file mode 100644 index 0000000..02de5b0 --- /dev/null +++ b/testing/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/testing/src/main/res/values/strings.xml b/testing/src/main/res/values/strings.xml new file mode 100644 index 0000000..3a5fe33 --- /dev/null +++ b/testing/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + testing + diff --git a/userlist/build.gradle b/userlist/build.gradle index 298aaa6..4bf1bc9 100644 --- a/userlist/build.gradle +++ b/userlist/build.gradle @@ -3,4 +3,6 @@ apply from: '../core.gradle' dependencies { implementation project(':base') + testImplementation project(':testing') + androidTestImplementation project(':testing') } From b720fb39e6845f80a8775eda0f87b915c29a83ad Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 6 Feb 2020 14:34:42 -0300 Subject: [PATCH 2/3] implement testing and create a simple test for Userlist --- app/build.gradle | 4 + .../com/hako/friendlists/MainApplication.kt | 6 +- .../viewmodel/NavigationViewmodel.kt | 20 ++++ base/build.gradle | 10 +- .../hako/base/domain/database/dao/AlbumDao.kt | 3 - .../hako/base/domain/database/dao/PhotoDao.kt | 3 - .../hako/base/domain/database/dao/UserDao.kt | 3 - .../base/domain/database/entities/User.kt | 2 +- core.gradle | 4 + testing/build.gradle | 10 +- .../com/hako/testing/TestingExtensions.kt | 10 ++ testing/versions.gradle | 24 +++++ tools/detekt.yml | 2 +- .../java/com/hako/userlist/feature/Mocks.kt | 29 ++++++ .../userlist/feature/UserlistFragmentTest.kt | 94 +++++++++++++++++++ 15 files changed, 199 insertions(+), 25 deletions(-) create mode 100644 testing/src/main/java/com/hako/testing/TestingExtensions.kt create mode 100644 testing/versions.gradle create mode 100644 userlist/src/androidTest/java/com/hako/userlist/feature/Mocks.kt create mode 100644 userlist/src/androidTest/java/com/hako/userlist/feature/UserlistFragmentTest.kt diff --git a/app/build.gradle b/app/build.gradle index 3d4fc2f..7b3f366 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,6 +37,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + testOptions { unitTests.returnDefaultValues = true } diff --git a/app/src/main/java/com/hako/friendlists/MainApplication.kt b/app/src/main/java/com/hako/friendlists/MainApplication.kt index 14faa3d..92db1b3 100644 --- a/app/src/main/java/com/hako/friendlists/MainApplication.kt +++ b/app/src/main/java/com/hako/friendlists/MainApplication.kt @@ -1,10 +1,8 @@ package com.hako.friendlists import android.app.Application -import com.hako.albumlist.di.albumListModules import com.hako.userlist.di.userlistModules import com.hako.friendlists.di.appModules -import com.hako.photolist.di.photoListModules import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin import timber.log.Timber @@ -28,9 +26,7 @@ class MainApplication : Application() { modules( listOf( appModules, - userlistModules, - albumListModules, - photoListModules + userlistModules ) ) } diff --git a/app/src/main/java/com/hako/friendlists/viewmodel/NavigationViewmodel.kt b/app/src/main/java/com/hako/friendlists/viewmodel/NavigationViewmodel.kt index aa1db70..09e853a 100644 --- a/app/src/main/java/com/hako/friendlists/viewmodel/NavigationViewmodel.kt +++ b/app/src/main/java/com/hako/friendlists/viewmodel/NavigationViewmodel.kt @@ -4,19 +4,35 @@ import android.os.Bundle import androidx.annotation.IdRes import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.hako.albumlist.di.albumListModules import com.hako.albumlist.feature.ALBUMLIST_FRAGMENT_BUNDLE_USER_ID import com.hako.albumlist.navigation.AlbumlistNavigation import com.hako.base.extensions.buildNavigation import com.hako.base.navigation.NavigationEvent import com.hako.friendlists.R +import com.hako.photolist.di.photoListModules import com.hako.photolist.feature.PHOTOLIST_FRAGMENT_BUNDLE_ALBUM_ID import com.hako.userlist.navigation.UserlistNavigation +import org.koin.core.context.loadKoinModules // This sets the fragment title, it's referenced in every navigation const val FRAGMENT_TITLE = "actionTitle" class NavigationViewmodel : ViewModel() { + // Load koin modules dynamically ;) + private val albums by lazy { + loadKoinModules(albumListModules) + } + + private val photos by lazy { + loadKoinModules(photoListModules) + } + + private fun injectAlbums() = albums + + private fun injectPhotos() = photos + val navigate = MutableLiveData>() fun onNavigationEvent(event: NavigationEvent) { @@ -28,6 +44,8 @@ class NavigationViewmodel : ViewModel() { } private fun handleUserlistNavigation(event: UserlistNavigation) { + injectAlbums() + when (event) { is UserlistNavigation.ClickedOnUser -> navigate.postValue( buildNavigation(R.id.action_userlistFragment_to_albumlistFragment, Bundle().apply { @@ -42,6 +60,8 @@ class NavigationViewmodel : ViewModel() { } private fun handleAlbumlistNavigation(event: AlbumlistNavigation) { + injectPhotos() + when (event) { is AlbumlistNavigation.ClickedOnAlbum -> navigate.postValue( buildNavigation(R.id.action_albumlistFragment_to_photolistFragment, Bundle().apply { diff --git a/base/build.gradle b/base/build.gradle index 440ca03..dd32c8e 100644 --- a/base/build.gradle +++ b/base/build.gradle @@ -52,12 +52,6 @@ dependencies { api deps.timber api deps.lottie api deps.picasso - //Testing - api deps.testing.junit - api deps.testing.koin - api deps.testing.core - api deps.testing.rules - api deps.testing.runner - api deps.testing.ext - api deps.testing.espresso + testImplementation project(':testing') + androidTestImplementation project(':testing') } diff --git a/base/src/main/java/com/hako/base/domain/database/dao/AlbumDao.kt b/base/src/main/java/com/hako/base/domain/database/dao/AlbumDao.kt index 6d22584..54abdad 100644 --- a/base/src/main/java/com/hako/base/domain/database/dao/AlbumDao.kt +++ b/base/src/main/java/com/hako/base/domain/database/dao/AlbumDao.kt @@ -15,9 +15,6 @@ interface AlbumDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun saveAll(entities: List) - @get:Query("SELECT * FROM ${AlbumEntity.TABLE_NAME}") - val all: List - @Query("SELECT * FROM ${AlbumEntity.TABLE_NAME} WHERE userId = :userId ORDER BY id ASC") fun getAlbums(userId: Int): List diff --git a/base/src/main/java/com/hako/base/domain/database/dao/PhotoDao.kt b/base/src/main/java/com/hako/base/domain/database/dao/PhotoDao.kt index 4ad3019..f6a2623 100644 --- a/base/src/main/java/com/hako/base/domain/database/dao/PhotoDao.kt +++ b/base/src/main/java/com/hako/base/domain/database/dao/PhotoDao.kt @@ -15,9 +15,6 @@ interface PhotoDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun saveAll(entities: List) - @get:Query("SELECT * FROM ${PhotoEntity.TABLE_NAME}") - val all: List - @Query("SELECT * FROM ${PhotoEntity.TABLE_NAME} WHERE albumId = :albumId ORDER BY id ASC") fun getPhotos(albumId: Int): List diff --git a/base/src/main/java/com/hako/base/domain/database/dao/UserDao.kt b/base/src/main/java/com/hako/base/domain/database/dao/UserDao.kt index 9bda2a6..f373651 100644 --- a/base/src/main/java/com/hako/base/domain/database/dao/UserDao.kt +++ b/base/src/main/java/com/hako/base/domain/database/dao/UserDao.kt @@ -18,9 +18,6 @@ interface UserDao { @Query("UPDATE ${UserEntity.TABLE_NAME} SET isFavorite = :favorite WHERE id = :id") fun saveFavorite(id: Int, favorite: Boolean): Int - @get:Query("SELECT * FROM ${UserEntity.TABLE_NAME}") - val all: List - @Query("SELECT * FROM ${UserEntity.TABLE_NAME} ORDER BY id ASC") fun getAllUsers(): List diff --git a/base/src/main/java/com/hako/base/domain/database/entities/User.kt b/base/src/main/java/com/hako/base/domain/database/entities/User.kt index 14e5be0..6756461 100644 --- a/base/src/main/java/com/hako/base/domain/database/entities/User.kt +++ b/base/src/main/java/com/hako/base/domain/database/entities/User.kt @@ -13,7 +13,7 @@ data class UserEntity( val email: String, val phone: String, val website: String, - val isFavorite: Boolean = true + val isFavorite: Boolean = false ) { companion object { const val TABLE_NAME = "users" diff --git a/core.gradle b/core.gradle index e0be423..db3b581 100644 --- a/core.gradle +++ b/core.gradle @@ -19,6 +19,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + testOptions { unitTests.returnDefaultValues = true } diff --git a/testing/build.gradle b/testing/build.gradle index d51b157..c3282a1 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -1,6 +1,14 @@ apply plugin: 'com.android.library' apply from: '../core.gradle' +apply from: 'versions.gradle' dependencies { - implementation project(':base') + api deps.testing.junit + api deps.testing.koin + api deps.testing.core + api deps.testing.rules + api deps.testing.runner + api deps.testing.ext + api deps.testing.espresso + api deps.testing.fragments } \ No newline at end of file diff --git a/testing/src/main/java/com/hako/testing/TestingExtensions.kt b/testing/src/main/java/com/hako/testing/TestingExtensions.kt new file mode 100644 index 0000000..50dd567 --- /dev/null +++ b/testing/src/main/java/com/hako/testing/TestingExtensions.kt @@ -0,0 +1,10 @@ +package com.hako.testing + +import androidx.annotation.RestrictTo +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.assertion.ViewAssertions.matches + +@RestrictTo(RestrictTo.Scope.TESTS) +fun String.isTextDisplayed() = onView(withText(this)).check(matches(ViewMatchers.isDisplayed())) \ No newline at end of file diff --git a/testing/versions.gradle b/testing/versions.gradle new file mode 100644 index 0000000..13b6f02 --- /dev/null +++ b/testing/versions.gradle @@ -0,0 +1,24 @@ +ext.deps = [:] + +def versions = [:] +versions.koin = "2.0.1" +versions.junit = "4.13" +versions.test = "1.2.0" +versions.test_ext = "1.1.1" +versions.espresso = "3.2.0" +versions.fragments = "1.2.0" + +def deps = [:] + +def testing = [:] +testing.junit = "junit:junit:$versions.junit" +testing.core = "androidx.test:core:$versions.test" +testing.rules = "androidx.test:rules:$versions.test" +testing.runner = "androidx.test:runner:$versions.test" +testing.ext = "androidx.test.ext:junit:$versions.test_ext" +testing.koin = "org.koin:koin-test:$versions.koin" +testing.espresso = "androidx.test.espresso:espresso-core:$versions.espresso" +testing.fragments = "androidx.fragment:fragment-testing:$versions.fragments" +deps.testing = testing + +ext.deps = deps diff --git a/tools/detekt.yml b/tools/detekt.yml index 0d5938a..3bcf79d 100644 --- a/tools/detekt.yml +++ b/tools/detekt.yml @@ -108,7 +108,7 @@ empty-blocks: EmptyForBlock: active: true EmptyFunctionBlock: - active: true + active: false ignoreOverridden: false EmptyIfBlock: active: true diff --git a/userlist/src/androidTest/java/com/hako/userlist/feature/Mocks.kt b/userlist/src/androidTest/java/com/hako/userlist/feature/Mocks.kt new file mode 100644 index 0000000..a6c771f --- /dev/null +++ b/userlist/src/androidTest/java/com/hako/userlist/feature/Mocks.kt @@ -0,0 +1,29 @@ +package com.hako.userlist.feature + +import com.hako.base.domain.database.dao.UserDao +import com.hako.base.domain.database.entities.UserEntity +import com.hako.userlist.domain.datasource.UserlistRemoteApi +import com.hako.userlist.model.User +import io.reactivex.Single + +class MockUserDao(private val userList: List) : UserDao { + override fun save(entity: UserEntity) {} + + override fun saveAll(entities: List) {} + + override fun saveFavorite(id: Int, favorite: Boolean) = 1 + + override fun getAllUsers() = userList + + override fun getFavoriteUsers() = userList.filter { it.isFavorite } + + override fun count() = userList.count() + + override fun nukeDatabase() {} +} + +class MockUserApi(private val userList: List) : UserlistRemoteApi { + override fun getUsers() = Single.fromCallable { getAllUsers() } + + private fun getAllUsers() = userList +} \ No newline at end of file diff --git a/userlist/src/androidTest/java/com/hako/userlist/feature/UserlistFragmentTest.kt b/userlist/src/androidTest/java/com/hako/userlist/feature/UserlistFragmentTest.kt new file mode 100644 index 0000000..c84d559 --- /dev/null +++ b/userlist/src/androidTest/java/com/hako/userlist/feature/UserlistFragmentTest.kt @@ -0,0 +1,94 @@ +package com.hako.userlist.feature + +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.platform.app.InstrumentationRegistry +import com.hako.base.domain.database.dao.UserDao +import com.hako.testing.isTextDisplayed +import com.hako.userlist.domain.datasource.UserlistRemoteApi +import com.hako.userlist.domain.usecase.GetFavoriteUsers +import com.hako.userlist.domain.usecase.GetUsers +import com.hako.userlist.domain.usecase.SetFavoriteStatus +import com.hako.userlist.model.User +import com.hako.userlist.model.toUserEntity +import com.hako.userlist.viewmodel.UserlistViewmodel +import org.junit.Before +import org.junit.Test +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.context.loadKoinModules +import org.koin.core.context.startKoin +import org.koin.dsl.module + +class UserlistFragmentTest { + + @Before + fun setupKoin() { + startKoin { + InstrumentationRegistry.getInstrumentation().targetContext + modules(module { + factory { GetUsers(get()) } + factory { GetFavoriteUsers(get()) } + factory { SetFavoriteStatus(get()) } + viewModel { UserlistViewmodel() } + }) + } + } + + @Test + fun shouldShowUserlist_withAllUsers() { + userlist { + initialState() + } should { + showTwoBasicUsers() + } + } + + private fun userlist(func: UserlistRobot.() -> Unit) = + UserlistRobot().apply { + func() + } +} + +class UserlistRobot { + + fun initialState() { + loadKoinModules( + module { + factory { MockUserDao(loadTwoBasicUsers().map { it.toUserEntity() }) } + factory { MockUserApi(loadTwoBasicUsers()) } + } + ) + launchFragmentInContainer() + } + + infix fun should(func: UserlistResult.() -> Unit) { + UserlistResult().apply { func() } + } + + private fun loadTwoBasicUsers() = listOf( + User( + 1, + "Marian Arriaga", + "mariancita", + "test@gmail.com", + "+56873912", + "www.test.com" + ), + User( + 2, + "Carlos Martinez", + "carlitos", + "test2@gmail.com", + "+56873912", + "www.test2.com" + ) + ) +} + +class UserlistResult { + fun showTwoBasicUsers() { + "Marian Arriaga".isTextDisplayed() + "mariancita".isTextDisplayed() + "Carlos Martinez".isTextDisplayed() + "carlitos".isTextDisplayed() + } +} \ No newline at end of file From 5ed203c3b5c949887036b5442bc431ae71dd0023 Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 6 Feb 2020 14:50:52 -0300 Subject: [PATCH 3/3] add test for favorite users --- .../userlist/feature/UserlistFragmentTest.kt | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/userlist/src/androidTest/java/com/hako/userlist/feature/UserlistFragmentTest.kt b/userlist/src/androidTest/java/com/hako/userlist/feature/UserlistFragmentTest.kt index c84d559..77c91d2 100644 --- a/userlist/src/androidTest/java/com/hako/userlist/feature/UserlistFragmentTest.kt +++ b/userlist/src/androidTest/java/com/hako/userlist/feature/UserlistFragmentTest.kt @@ -3,6 +3,7 @@ package com.hako.userlist.feature import androidx.fragment.app.testing.launchFragmentInContainer import androidx.test.platform.app.InstrumentationRegistry import com.hako.base.domain.database.dao.UserDao +import com.hako.base.domain.database.entities.UserEntity import com.hako.testing.isTextDisplayed import com.hako.userlist.domain.datasource.UserlistRemoteApi import com.hako.userlist.domain.usecase.GetFavoriteUsers @@ -11,11 +12,13 @@ import com.hako.userlist.domain.usecase.SetFavoriteStatus import com.hako.userlist.model.User import com.hako.userlist.model.toUserEntity import com.hako.userlist.viewmodel.UserlistViewmodel +import org.junit.After import org.junit.Before import org.junit.Test import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.context.loadKoinModules import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin import org.koin.dsl.module class UserlistFragmentTest { @@ -33,15 +36,29 @@ class UserlistFragmentTest { } } + @After + fun killKoin() { + stopKoin() + } + @Test fun shouldShowUserlist_withAllUsers() { userlist { - initialState() + withTwoBasicUsers() } should { showTwoBasicUsers() } } + @Test + fun shouldShowOnlyFavoriteUserlist_withAllUsers() { + userlist { + withTwoBasicOneFavoriteUsers() + } should { + showOnlyOneFavorite() + } + } + private fun userlist(func: UserlistRobot.() -> Unit) = UserlistRobot().apply { func() @@ -50,7 +67,7 @@ class UserlistFragmentTest { class UserlistRobot { - fun initialState() { + fun withTwoBasicUsers() { loadKoinModules( module { factory { MockUserDao(loadTwoBasicUsers().map { it.toUserEntity() }) } @@ -60,6 +77,16 @@ class UserlistRobot { launchFragmentInContainer() } + fun withTwoBasicOneFavoriteUsers() { + loadKoinModules( + module { + factory { MockUserDao(loadTwoUsersOneFavorite()) } + factory { MockUserApi(loadTwoBasicUsers()) } + } + ) + launchFragmentInContainer() + } + infix fun should(func: UserlistResult.() -> Unit) { UserlistResult().apply { func() } } @@ -82,6 +109,26 @@ class UserlistRobot { "www.test2.com" ) ) + + private fun loadTwoUsersOneFavorite() = listOf( + UserEntity( + 1, + "Marian Arriaga", + "mariancita", + "test@gmail.com", + "+56873912", + "www.test.com", + true + ), + UserEntity( + 2, + "Carlos Martinez", + "carlitos", + "test2@gmail.com", + "+56873912", + "www.test2.com" + ) + ) } class UserlistResult { @@ -91,4 +138,9 @@ class UserlistResult { "Carlos Martinez".isTextDisplayed() "carlitos".isTextDisplayed() } + + fun showOnlyOneFavorite() { + "Marian Arriaga".isTextDisplayed() + "mariancita".isTextDisplayed() + } } \ No newline at end of file