From 87ea6954b8ebba8f269b9989b204076b68a0f983 Mon Sep 17 00:00:00 2001 From: Josef Raska <6277721+jraska@users.noreply.github.com> Date: Fri, 30 Apr 2021 22:22:02 +0200 Subject: [PATCH 1/6] Working Android Test --- app/build.gradle | 2 + .../com/jraska/github/client/TestUITestApp.kt | 13 ++- .../github/client/http/ReplayHttpModule.kt | 9 +- app/src/debug/AndroidManifest.xml | 7 +- .../debug/res/xml/network_security_config.xml | 6 ++ .../client/core/android/LinkLauncher.kt | 5 +- core-android-testing/.gitignore | 1 + core-android-testing/build.gradle | 35 ++++++++ .../src/main/AndroidManifest.xml | 1 + .../client/android/test/DeepLinksRecorder.kt | 32 +++++++ .../android/test/FakeAndroidCoreModule.kt | 46 ++++++++++ .../test/FakeDeepLinkRecordingModule.kt | 32 +++++++ core-testing/build.gradle | 1 + .../github/client/http/FakeHttpModule.kt | 6 +- .../com/jraska/github/client/http/HttpTest.kt | 39 ++++++++- .../client/repo/RepoDetailViewModelTest.kt | 14 ++- .../client/repo/di/TestRepoComponent.kt | 2 - feature/users/build.gradle | 11 ++- .../github/client/users/test/TestRunner.kt | 12 +++ .../client/users/test/TestUsersComponent.kt | 42 +++++++++ .../client/users/test/TestUsersUITestApp.kt | 10 +++ .../client/users/test/UsersActivityTest.kt | 87 +++++++++++++++++++ feature/users/src/main/AndroidManifest.xml | 10 ++- .../client/users/UserDetailViewModelTest.kt | 9 +- .../github/client/users/UsersViewModelTest.kt | 11 ++- .../client/users/di/TestUsersComponent.kt | 2 - .../model/GitHubApiUsersRepositoryTest.kt | 5 +- settings.gradle | 1 + 28 files changed, 407 insertions(+), 44 deletions(-) create mode 100644 app/src/debug/res/xml/network_security_config.xml create mode 100644 core-android-testing/.gitignore create mode 100644 core-android-testing/build.gradle create mode 100644 core-android-testing/src/main/AndroidManifest.xml create mode 100644 core-android-testing/src/main/java/com/jraska/github/client/android/test/DeepLinksRecorder.kt create mode 100644 core-android-testing/src/main/java/com/jraska/github/client/android/test/FakeAndroidCoreModule.kt create mode 100644 core-android-testing/src/main/java/com/jraska/github/client/android/test/FakeDeepLinkRecordingModule.kt create mode 100644 feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestRunner.kt create mode 100644 feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestUsersComponent.kt create mode 100644 feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestUsersUITestApp.kt create mode 100644 feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt diff --git a/app/build.gradle b/app/build.gradle index a46475e8..27ea00c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -157,10 +157,12 @@ dependencies { androidTestImplementation 'com.airbnb.okreplay:espresso:1.6.0' androidTestImplementation 'com.squareup.rx.idler:rx3-idler:0.11.0' androidTestImplementation project(':core-testing') + androidTestImplementation project(':core-android-testing') androidTestImplementation rootProject.ext.retrofit androidTestImplementation rootProject.ext.retrofitGsonConverter androidTestImplementation rootProject.ext.retrofitRxJavaAdapter + androidTestImplementation okHttpMockWebServer kaptAndroidTest rootProject.ext.daggerAnnotationProcessor } diff --git a/app/src/androidTest/java/com/jraska/github/client/TestUITestApp.kt b/app/src/androidTest/java/com/jraska/github/client/TestUITestApp.kt index 822258e7..923151fa 100644 --- a/app/src/androidTest/java/com/jraska/github/client/TestUITestApp.kt +++ b/app/src/androidTest/java/com/jraska/github/client/TestUITestApp.kt @@ -5,6 +5,8 @@ import androidx.test.platform.app.InstrumentationRegistry import com.jraska.github.client.core.android.BaseApp import com.jraska.github.client.core.android.ServiceModel import com.jraska.github.client.http.ReplayHttpModule +import com.jraska.github.client.users.test.DeepLinkRecordingComponent +import com.jraska.github.client.users.test.FakeDeepLinkRecordingModule import dagger.BindsInstance import dagger.Component import javax.inject.Singleton @@ -30,8 +32,15 @@ class TestUITestApp : BaseApp() { } @Singleton -@Component(modules = [SharedModules::class, FakeCoreModule::class, ReplayHttpModule::class]) -interface TestAppComponent : AppComponent { +@Component( + modules = [ + SharedModules::class, + FakeCoreModule::class, + ReplayHttpModule::class, + FakeDeepLinkRecordingModule::class + ] +) +interface TestAppComponent : AppComponent, DeepLinkRecordingComponent { val config: FakeConfig @Component.Factory diff --git a/app/src/androidTest/java/com/jraska/github/client/http/ReplayHttpModule.kt b/app/src/androidTest/java/com/jraska/github/client/http/ReplayHttpModule.kt index 7d59b37c..3d4d10ec 100644 --- a/app/src/androidTest/java/com/jraska/github/client/http/ReplayHttpModule.kt +++ b/app/src/androidTest/java/com/jraska/github/client/http/ReplayHttpModule.kt @@ -10,7 +10,6 @@ import dagger.Module import dagger.Provides import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import okreplay.AndroidTapeRoot import okreplay.OkReplayConfig @@ -34,7 +33,6 @@ object ReplayHttpModule { } private val REPLAY_MEDIATOR = OkReplayMediator() - private const val NETWORK_ERROR_MESSAGE = "You are trying to do network requests in tests you naughty developer!" private fun createRetrofit(): Retrofit { return Retrofit.Builder() @@ -85,12 +83,7 @@ object ReplayHttpModule { REPLAY_MEDIATOR.configure(builder) - val noNetworkInterceptor = object : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - throw UnsupportedOperationException(NETWORK_ERROR_MESSAGE) - } - } - builder.addNetworkInterceptor(noNetworkInterceptor) + builder.addInterceptor(MockWebServerInterceptor) return builder.build() } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index b1dbf6cd..c104b5f4 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,7 +1,8 @@ - + - + diff --git a/app/src/debug/res/xml/network_security_config.xml b/app/src/debug/res/xml/network_security_config.xml new file mode 100644 index 00000000..d4950ca4 --- /dev/null +++ b/app/src/debug/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + localhost + + diff --git a/core-android-api/src/main/java/com/jraska/github/client/core/android/LinkLauncher.kt b/core-android-api/src/main/java/com/jraska/github/client/core/android/LinkLauncher.kt index c9903093..a238c579 100644 --- a/core-android-api/src/main/java/com/jraska/github/client/core/android/LinkLauncher.kt +++ b/core-android-api/src/main/java/com/jraska/github/client/core/android/LinkLauncher.kt @@ -14,7 +14,8 @@ interface LinkLauncher { } enum class Priority(val value: Int) { - EXACT_MATCH(0), - PATH_LENGTH(1) + TESTING(0), + EXACT_MATCH(1), + PATH_LENGTH(2) } } diff --git a/core-android-testing/.gitignore b/core-android-testing/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/core-android-testing/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core-android-testing/build.gradle b/core-android-testing/build.gradle new file mode 100644 index 00000000..e447a8ae --- /dev/null +++ b/core-android-testing/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 30 + defaultConfig { + minSdkVersion 24 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation project(':core-api') + implementation project(':core-android-api') + + kapt rootProject.ext.daggerAnnotationProcessor + implementation rootProject.ext.dagger + + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + + implementation okHttpMockWebServer + implementation rootProject.ext.okHttp + implementation 'com.squareup.okio:okio:2.10.0' + + implementation 'com.squareup.rx.idler:rx3-idler:0.11.0' +} diff --git a/core-android-testing/src/main/AndroidManifest.xml b/core-android-testing/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a9624e89 --- /dev/null +++ b/core-android-testing/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/core-android-testing/src/main/java/com/jraska/github/client/android/test/DeepLinksRecorder.kt b/core-android-testing/src/main/java/com/jraska/github/client/android/test/DeepLinksRecorder.kt new file mode 100644 index 00000000..9d4c2a84 --- /dev/null +++ b/core-android-testing/src/main/java/com/jraska/github/client/android/test/DeepLinksRecorder.kt @@ -0,0 +1,32 @@ +package com.jraska.github.client.android.test + +import android.app.Activity +import com.jraska.github.client.core.android.LinkLauncher +import okhttp3.HttpUrl + +class DeepLinksRecorder : LinkLauncher { + val linksLaunched = mutableListOf() + private var swallowLinks = false + + override fun priority() = LinkLauncher.Priority.TESTING + + override fun launch(inActivity: Activity, deepLink: HttpUrl): LinkLauncher.Result { + linksLaunched.add(deepLink) + + return if (swallowLinks) { + LinkLauncher.Result.LAUNCHED // swallows link only as recorded + } else { + LinkLauncher.Result.NOT_LAUNCHED + } + } + + fun usingLinkRecording(block: (DeepLinksRecorder) -> Unit) { + try { + swallowLinks = true + block(this) + } finally { + swallowLinks = false + linksLaunched.clear() + } + } +} diff --git a/core-android-testing/src/main/java/com/jraska/github/client/android/test/FakeAndroidCoreModule.kt b/core-android-testing/src/main/java/com/jraska/github/client/android/test/FakeAndroidCoreModule.kt new file mode 100644 index 00000000..f192a0e0 --- /dev/null +++ b/core-android-testing/src/main/java/com/jraska/github/client/android/test/FakeAndroidCoreModule.kt @@ -0,0 +1,46 @@ +package com.jraska.github.client.android.test + +import android.app.Application +import androidx.lifecycle.ViewModel +import com.jraska.github.client.core.android.OnAppCreate +import com.jraska.github.client.core.android.ServiceModel +import com.squareup.rx3.idler.Rx3Idler +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet +import io.reactivex.rxjava3.plugins.RxJavaPlugins + +@Module +object FakeAndroidCoreModule { + + @Provides + @IntoMap + @ClassKey(ServiceModel::class) + internal fun provideServiceModel(): ServiceModel { + return object : ServiceModel {} // Make sure the collection is not empty + } + + @Provides + @IntoMap + @ClassKey(ViewModel::class) + internal fun provideViewModel(): ViewModel { + return object : ViewModel() {} // Make sure the collection is not empty + } + + @Provides + @IntoSet + fun startupRxIdlers() : OnAppCreate { + return object : OnAppCreate { + override fun onCreate(app: Application) { + RxJavaPlugins.setInitComputationSchedulerHandler( + Rx3Idler.create("RxJava 3.x Computation Scheduler") + ) + RxJavaPlugins.setInitIoSchedulerHandler( + Rx3Idler.create("RxJava 3.x IO Scheduler") + ) + } + } + } +} diff --git a/core-android-testing/src/main/java/com/jraska/github/client/android/test/FakeDeepLinkRecordingModule.kt b/core-android-testing/src/main/java/com/jraska/github/client/android/test/FakeDeepLinkRecordingModule.kt new file mode 100644 index 00000000..2b17be4b --- /dev/null +++ b/core-android-testing/src/main/java/com/jraska/github/client/android/test/FakeDeepLinkRecordingModule.kt @@ -0,0 +1,32 @@ +package com.jraska.github.client.users.test + +import androidx.test.platform.app.InstrumentationRegistry +import com.jraska.github.client.android.test.DeepLinksRecorder +import com.jraska.github.client.core.android.BaseApp +import com.jraska.github.client.core.android.LinkLauncher +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import javax.inject.Singleton + +@Module +object FakeDeepLinkRecordingModule { + + @Provides + @Singleton + fun recordingLauncher() = DeepLinksRecorder() + + @Provides + @IntoSet + fun linkLauncher(launcher: DeepLinksRecorder): LinkLauncher = launcher +} + +interface DeepLinkRecordingComponent { + val deepLinksRecorder: DeepLinksRecorder +} + +fun usingLinkRecording(block: (DeepLinksRecorder) -> Unit) { + val appComponent = (InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as BaseApp).appComponent + + (appComponent as DeepLinkRecordingComponent).deepLinksRecorder.usingLinkRecording(block) +} diff --git a/core-testing/build.gradle b/core-testing/build.gradle index a6f57082..b00c0494 100644 --- a/core-testing/build.gradle +++ b/core-testing/build.gradle @@ -10,6 +10,7 @@ dependencies { kapt rootProject.ext.daggerAnnotationProcessor implementation rootProject.ext.dagger + implementation 'junit:junit:4.13.2' implementation 'io.reactivex.rxjava3:rxjava:3.0.12' implementation rootProject.ext.retrofit diff --git a/core-testing/src/main/java/com/jraska/github/client/http/FakeHttpModule.kt b/core-testing/src/main/java/com/jraska/github/client/http/FakeHttpModule.kt index bf5758b5..d332ddc0 100644 --- a/core-testing/src/main/java/com/jraska/github/client/http/FakeHttpModule.kt +++ b/core-testing/src/main/java/com/jraska/github/client/http/FakeHttpModule.kt @@ -7,12 +7,8 @@ import javax.inject.Singleton @Module class FakeHttpModule { - val mockWebServer = MockWebServer() - - @Provides // will be singleton anyway as we have field - fun mockWebServer() = mockWebServer @Provides @Singleton - fun provideRetrofit() = HttpTest.retrofit(mockWebServer.url("/")) + fun provideRetrofit() = HttpTest.retrofit() } diff --git a/core-testing/src/main/java/com/jraska/github/client/http/HttpTest.kt b/core-testing/src/main/java/com/jraska/github/client/http/HttpTest.kt index bb65a5b2..01592786 100644 --- a/core-testing/src/main/java/com/jraska/github/client/http/HttpTest.kt +++ b/core-testing/src/main/java/com/jraska/github/client/http/HttpTest.kt @@ -1,22 +1,32 @@ package com.jraska.github.client.http import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer +import org.junit.rules.ExternalResource import retrofit2.Retrofit import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import java.io.File object HttpTest { - fun retrofit(baseUrl: HttpUrl): Retrofit { + private val DEFAULT_BASE_URL = "https://api.github.com".toHttpUrl() + + fun retrofit(baseUrl: HttpUrl = DEFAULT_BASE_URL): Retrofit { return Retrofit.Builder() .baseUrl(baseUrl) - .client(OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor { println(it) }.apply { - level = HttpLoggingInterceptor.Level.BASIC - }).build()) + .client( + OkHttpClient.Builder() + .also { if (baseUrl == DEFAULT_BASE_URL) it.addInterceptor(MockWebServerInterceptor) } // TODO: 30/4/21 Hack - unification of network mocking needed + .addInterceptor(HttpLoggingInterceptor { println(it) }.apply { + level = HttpLoggingInterceptor.Level.BASIC + }).build() + ) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava3CallAdapterFactory.createSynchronous()) .build() @@ -51,3 +61,24 @@ internal fun json(path: String): String { return String(file.readBytes()) } +object MockWebServerInterceptor : Interceptor { + var mockWebServer: MockWebServer? = null + + override fun intercept(chain: Interceptor.Chain): Response { + val webServer = mockWebServer ?: throw UnsupportedOperationException("You are trying to do network requests in tests you naughty developer!") + + val newRequest = chain.request().newBuilder().url(webServer.url(chain.request().url.encodedPath)).build() + return chain.proceed(newRequest) + } +} + +class MockWebServerInterceptorRule(private val mockWebServer: MockWebServer) : ExternalResource() { + override fun before() { + MockWebServerInterceptor.mockWebServer = mockWebServer + } + + override fun after() { + MockWebServerInterceptor.mockWebServer = null + } +} + diff --git a/feature/repo/src/test/java/com/jraska/github/client/repo/RepoDetailViewModelTest.kt b/feature/repo/src/test/java/com/jraska/github/client/repo/RepoDetailViewModelTest.kt index 5fbbed7f..c9e0ff72 100644 --- a/feature/repo/src/test/java/com/jraska/github/client/repo/RepoDetailViewModelTest.kt +++ b/feature/repo/src/test/java/com/jraska/github/client/repo/RepoDetailViewModelTest.kt @@ -1,11 +1,13 @@ package com.jraska.github.client.repo import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.jraska.github.client.http.MockWebServerInterceptorRule import com.jraska.github.client.http.enqueue import com.jraska.github.client.repo.di.DaggerTestRepoComponent import com.jraska.github.client.repo.di.TestRepoComponent import com.jraska.github.client.repo.model.GitHubApiRepoRepositoryTest import com.jraska.livedata.test +import okhttp3.mockwebserver.MockWebServer import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Rule @@ -16,6 +18,10 @@ internal class RepoDetailViewModelTest { @get:Rule val testRule = InstantTaskExecutorRule() + @get:Rule val mockWebServer = MockWebServer() + + @get:Rule val mockWebServerInterceptorRule = MockWebServerInterceptorRule(mockWebServer) + lateinit var component: TestRepoComponent lateinit var repoDetailViewModel: RepoDetailViewModel @@ -27,8 +33,8 @@ internal class RepoDetailViewModelTest { @Test fun whenLoadThenLoadsProperRepoDetail() { - component.mockWebServer.enqueue("response/repo_detail.json") - component.mockWebServer.enqueue("response/repo_pulls.json") + mockWebServer.enqueue("response/repo_detail.json") + mockWebServer.enqueue("response/repo_pulls.json") val showRepo = repoDetailViewModel.repoDetail("jraska/github-client") .test() @@ -46,8 +52,8 @@ internal class RepoDetailViewModelTest { @Test fun whenErrorThenLoadsErrorState() { - component.mockWebServer.enqueue("response/error.json") - component.mockWebServer.enqueue("response/error.json") + mockWebServer.enqueue("response/error.json") + mockWebServer.enqueue("response/error.json") val state = repoDetailViewModel.repoDetail("jraska/github-client") .test() diff --git a/feature/repo/src/test/java/com/jraska/github/client/repo/di/TestRepoComponent.kt b/feature/repo/src/test/java/com/jraska/github/client/repo/di/TestRepoComponent.kt index 94ecd28e..d9c541b2 100644 --- a/feature/repo/src/test/java/com/jraska/github/client/repo/di/TestRepoComponent.kt +++ b/feature/repo/src/test/java/com/jraska/github/client/repo/di/TestRepoComponent.kt @@ -13,7 +13,5 @@ import javax.inject.Singleton internal interface TestRepoComponent { fun repoDetailViewModel(): RepoDetailViewModel - val mockWebServer: MockWebServer - val fakeSnackbarDisplay: FakeSnackbarDisplay } diff --git a/feature/users/build.gradle b/feature/users/build.gradle index c7e6f991..2b2f1b44 100644 --- a/feature/users/build.gradle +++ b/feature/users/build.gradle @@ -7,7 +7,7 @@ android { defaultConfig { minSdkVersion 24 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "com.jraska.github.client.users.test.TestRunner" } compileOptions { @@ -56,4 +56,13 @@ dependencies { androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'org.assertj:assertj-core:3.19.0' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test:rules:1.3.0' + + kaptAndroidTest rootProject.ext.daggerAnnotationProcessor + androidTestImplementation project(':core-testing') + androidTestImplementation project(':core-android-testing') + androidTestImplementation project(':core') + androidTestImplementation okHttpMockWebServer + androidTestImplementation 'com.jakewharton.timber:timber:4.7.1' } diff --git a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestRunner.kt b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestRunner.kt new file mode 100644 index 00000000..c4f89f04 --- /dev/null +++ b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestRunner.kt @@ -0,0 +1,12 @@ +package com.jraska.github.client.users.test + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner + +@Suppress("unused") // build.gradle +class TestRunner : AndroidJUnitRunner() { + override fun newApplication(cl: ClassLoader, className: String, context: Context): Application { + return super.newApplication(cl, TestUsersUITestApp::class.java.name, context) + } +} diff --git a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestUsersComponent.kt b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestUsersComponent.kt new file mode 100644 index 00000000..f55f40a1 --- /dev/null +++ b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestUsersComponent.kt @@ -0,0 +1,42 @@ +package com.jraska.github.client.users.test + +import android.content.Context +import com.jraska.github.client.FakeCoreModule +import com.jraska.github.client.FakeWebLinkModule +import com.jraska.github.client.android.test.FakeAndroidCoreModule +import com.jraska.github.client.core.android.AppBaseComponent +import com.jraska.github.client.core.android.CoreAndroidModule +import com.jraska.github.client.http.HttpTest +import com.jraska.github.client.users.UsersModule +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Singleton +@Component( + modules = [ + FakeCoreModule::class, + FakeAndroidCoreModule::class, + FakeHttpModule::class, + FakeWebLinkModule::class, + FakeDeepLinkRecordingModule::class, + UsersModule::class, + CoreAndroidModule::class + ] +) +interface TestUsersComponent : AppBaseComponent, DeepLinkRecordingComponent { + @Component.Factory + interface Factory { + fun create(@BindsInstance context: Context): TestUsersComponent + } +} + +// TODO: 21/04/2021 Unify the network mocking with app to have only one fake http module +@Module +class FakeHttpModule { + @Provides + @Singleton + fun provideRetrofit() = HttpTest.retrofit() +} diff --git a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestUsersUITestApp.kt b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestUsersUITestApp.kt new file mode 100644 index 00000000..2652fcfe --- /dev/null +++ b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/TestUsersUITestApp.kt @@ -0,0 +1,10 @@ +package com.jraska.github.client.users.test + +import com.jraska.github.client.core.android.BaseApp + +class TestUsersUITestApp : BaseApp() { + override val appComponent + get() = testUsersComponent + + val testUsersComponent by lazy { DaggerTestUsersComponent.factory().create(this) } +} diff --git a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt new file mode 100644 index 00000000..c37482c9 --- /dev/null +++ b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt @@ -0,0 +1,87 @@ +package com.jraska.github.client.users.test + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.rule.ActivityTestRule +import com.jraska.github.client.http.MockWebServerInterceptor +import com.jraska.github.client.http.MockWebServerInterceptorRule +import com.jraska.github.client.users.ui.UsersActivity +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class UsersActivityTest { + + @get:Rule + val mockWebServer = MockWebServer() + + @get:Rule val mockWebServerInterceptorRule = MockWebServerInterceptorRule(mockWebServer) + + @get:Rule + val rule = ActivityTestRule(UsersActivity::class.java, false, false) + + @Test + fun testLaunches() { + mockWebServer.enqueue(twoUsersResponse()) + rule.launchActivity(null) + + usingLinkRecording { + onView(withText("defunkt")).perform(click()) + + val lastScreen = it.linksLaunched.last() + assertThat(lastScreen.toString()).isEqualTo("https://github.com/defunkt") + + onView(withText("mojombo")).perform(click()) + val nextScreen = it.linksLaunched.last() + assertThat(nextScreen.toString()).isEqualTo("https://github.com/mojombo") + } + } + + private fun twoUsersResponse() = MockResponse().setResponseCode(200).setBody( + "[\n" + + " {\n" + + " \"login\": \"mojombo\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDQ6VXNlcjE=\",\n" + + " \"avatar_url\": \"https://avatars.githubusercontent.com/u/1?v=4\",\n" + + " \"gravatar_id\": \"\",\n" + + " \"url\": \"https://api.github.com/users/mojombo\",\n" + + " \"html_url\": \"https://github.com/mojombo\",\n" + + " \"followers_url\": \"https://api.github.com/users/mojombo/followers\",\n" + + " \"following_url\": \"https://api.github.com/users/mojombo/following{/other_user}\",\n" + + " \"gists_url\": \"https://api.github.com/users/mojombo/gists{/gist_id}\",\n" + + " \"starred_url\": \"https://api.github.com/users/mojombo/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\": \"https://api.github.com/users/mojombo/subscriptions\",\n" + + " \"organizations_url\": \"https://api.github.com/users/mojombo/orgs\",\n" + + " \"repos_url\": \"https://api.github.com/users/mojombo/repos\",\n" + + " \"events_url\": \"https://api.github.com/users/mojombo/events{/privacy}\",\n" + + " \"received_events_url\": \"https://api.github.com/users/mojombo/received_events\",\n" + + " \"type\": \"User\",\n" + + " \"site_admin\": false\n" + + " },\n" + + " {\n" + + " \"login\": \"defunkt\",\n" + + " \"id\": 2,\n" + + " \"node_id\": \"MDQ6VXNlcjI=\",\n" + + " \"avatar_url\": \"https://avatars0.githubusercontent.com/u/2?v=4\",\n" + + " \"gravatar_id\": \"\",\n" + + " \"url\": \"https://api.github.com/users/defunkt\",\n" + + " \"html_url\": \"https://github.com/defunkt\",\n" + + " \"followers_url\": \"https://api.github.com/users/defunkt/followers\",\n" + + " \"following_url\": \"https://api.github.com/users/defunkt/following{/other_user}\",\n" + + " \"gists_url\": \"https://api.github.com/users/defunkt/gists{/gist_id}\",\n" + + " \"starred_url\": \"https://api.github.com/users/defunkt/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\": \"https://api.github.com/users/defunkt/subscriptions\",\n" + + " \"organizations_url\": \"https://api.github.com/users/defunkt/orgs\",\n" + + " \"repos_url\": \"https://api.github.com/users/defunkt/repos\",\n" + + " \"events_url\": \"https://api.github.com/users/defunkt/events{/privacy}\",\n" + + " \"received_events_url\": \"https://api.github.com/users/defunkt/received_events\",\n" + + " \"type\": \"User\",\n" + + " \"site_admin\": true\n" + + " }]" + ) +} diff --git a/feature/users/src/main/AndroidManifest.xml b/feature/users/src/main/AndroidManifest.xml index 08cbf5f5..dd567df4 100644 --- a/feature/users/src/main/AndroidManifest.xml +++ b/feature/users/src/main/AndroidManifest.xml @@ -1,8 +1,12 @@ - + + + - - + + diff --git a/feature/users/src/test/java/com/jraska/github/client/users/UserDetailViewModelTest.kt b/feature/users/src/test/java/com/jraska/github/client/users/UserDetailViewModelTest.kt index f6b7b474..7bbf27af 100644 --- a/feature/users/src/test/java/com/jraska/github/client/users/UserDetailViewModelTest.kt +++ b/feature/users/src/test/java/com/jraska/github/client/users/UserDetailViewModelTest.kt @@ -1,6 +1,7 @@ package com.jraska.github.client.users import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.jraska.github.client.http.MockWebServerInterceptorRule import com.jraska.github.client.http.enqueue import com.jraska.github.client.http.onUrlPartReturn import com.jraska.github.client.http.onUrlReturn @@ -21,14 +22,16 @@ class UserDetailViewModelTest { @get:Rule val testRule = InstantTaskExecutorRule() + @get:Rule val mockWebServer = MockWebServer() + + @get:Rule val mockWebServerInterceptorRule = MockWebServerInterceptorRule(mockWebServer) + private lateinit var viewModel: UserDetailViewModel - private lateinit var mockWebServer: MockWebServer @Before fun before() { val component = DaggerTestUsersComponent.create() viewModel = component.userDetailViewModel() - mockWebServer = component.mockWebServer } @Test @@ -39,8 +42,6 @@ class UserDetailViewModelTest { val observer = viewModel.userDetail("jraska") .test() -// Thread.sleep(200) - val displayUser = observer.value() as UserDetailViewModel.ViewState.DisplayUser assertThat(displayUser.user).usingRecursiveComparison().isEqualTo(testDetail()) diff --git a/feature/users/src/test/java/com/jraska/github/client/users/UsersViewModelTest.kt b/feature/users/src/test/java/com/jraska/github/client/users/UsersViewModelTest.kt index 73fa80f2..ed7efb75 100644 --- a/feature/users/src/test/java/com/jraska/github/client/users/UsersViewModelTest.kt +++ b/feature/users/src/test/java/com/jraska/github/client/users/UsersViewModelTest.kt @@ -1,9 +1,12 @@ package com.jraska.github.client.users import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.jraska.github.client.http.MockWebServerInterceptor +import com.jraska.github.client.http.MockWebServerInterceptorRule import com.jraska.github.client.http.enqueue import com.jraska.github.client.users.di.DaggerTestUsersComponent import com.jraska.livedata.test +import okhttp3.mockwebserver.MockWebServer import org.junit.Before import org.junit.Rule import org.junit.Test @@ -11,6 +14,10 @@ import org.junit.Test class UsersViewModelTest { @get:Rule val testRule = InstantTaskExecutorRule() + @get:Rule val mockWebServer = MockWebServer() + + @get:Rule val mockWebServerInterceptorRule = MockWebServerInterceptorRule(mockWebServer) + private lateinit var viewModel: UsersViewModel @Before @@ -18,8 +25,8 @@ class UsersViewModelTest { val component = DaggerTestUsersComponent.create() viewModel = component.usersViewModel() - component.mockWebServer.enqueue("response/users.json") - component.mockWebServer.enqueue("response/users_with_extra.json") + mockWebServer.enqueue("response/users.json") + mockWebServer.enqueue("response/users_with_extra.json") } @Test diff --git a/feature/users/src/test/java/com/jraska/github/client/users/di/TestUsersComponent.kt b/feature/users/src/test/java/com/jraska/github/client/users/di/TestUsersComponent.kt index c674c40f..f174d322 100644 --- a/feature/users/src/test/java/com/jraska/github/client/users/di/TestUsersComponent.kt +++ b/feature/users/src/test/java/com/jraska/github/client/users/di/TestUsersComponent.kt @@ -16,6 +16,4 @@ internal interface TestUsersComponent { fun usersViewModel(): UsersViewModel fun userDetailViewModel(): UserDetailViewModel - - val mockWebServer: MockWebServer } diff --git a/feature/users/src/test/java/com/jraska/github/client/users/model/GitHubApiUsersRepositoryTest.kt b/feature/users/src/test/java/com/jraska/github/client/users/model/GitHubApiUsersRepositoryTest.kt index 58ffd9d3..07865217 100644 --- a/feature/users/src/test/java/com/jraska/github/client/users/model/GitHubApiUsersRepositoryTest.kt +++ b/feature/users/src/test/java/com/jraska/github/client/users/model/GitHubApiUsersRepositoryTest.kt @@ -10,11 +10,14 @@ import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.mockwebserver.MockWebServer import org.assertj.core.api.Assertions.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test class GitHubApiUsersRepositoryTest { + @get:Rule val mockWebServer = MockWebServer() + internal lateinit var repository: GitHubApiUsersRepository @Before @@ -26,8 +29,6 @@ class GitHubApiUsersRepositoryTest { fun getsUsersProperly() { mockWebServer.enqueue("response/users.json") - println(Thread.currentThread().id) - val users = repository.getUsers(0) .test() .values() diff --git a/settings.gradle b/settings.gradle index 171da33a..4a3bab6f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,7 @@ include ':app', ':core', ':core-api', ':core-testing', + ':core-android-testing', ':core-android-api', ':navigation-api', ':feature:ui-common-api', From 5495e5034f00123dfc45f2165d02d95d41877b08 Mon Sep 17 00:00:00 2001 From: Josef Raska <6277721+jraska@users.noreply.github.com> Date: Fri, 30 Apr 2021 22:38:14 +0200 Subject: [PATCH 2/6] Remove manual launching --- .../com/jraska/github/client/users/test/UsersActivityTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt index c37482c9..dbdc63d2 100644 --- a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt +++ b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt @@ -22,12 +22,11 @@ class UsersActivityTest { @get:Rule val mockWebServerInterceptorRule = MockWebServerInterceptorRule(mockWebServer) @get:Rule - val rule = ActivityTestRule(UsersActivity::class.java, false, false) + val rule = ActivityTestRule(UsersActivity::class.java) @Test fun testLaunches() { mockWebServer.enqueue(twoUsersResponse()) - rule.launchActivity(null) usingLinkRecording { onView(withText("defunkt")).perform(click()) From 250f3f6e4dc0e726e4213d1800d657031928fff6 Mon Sep 17 00:00:00 2001 From: Josef Raska <6277721+jraska@users.noreply.github.com> Date: Fri, 30 Apr 2021 22:43:34 +0200 Subject: [PATCH 3/6] Add assets as well --- app/build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 27ea00c0..6cf43d82 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -81,11 +81,15 @@ android { exclude 'META-INF/licenses/ASM' } - if (gradle.startParameter.taskNames.any { it.contains("Firebase") }) { + if (!gradle.startParameter.taskNames.any { it.contains("Firebase") }) { sourceSets.androidTest.java { srcDirs += ["../feature/about/src/androidTest/java"] srcDirs += ["../feature/users/src/androidTest/java"] } + sourceSets.androidTest.assets { + srcDirs += ["../feature/about/src/androidTest/assets"] + srcDirs += ["../feature/users/src/androidTest/assets"] + } } } From 94d8aaf68df9ec4f36d848a77af1556b3e0ac4ad Mon Sep 17 00:00:00 2001 From: Josef Raska <6277721+jraska@users.noreply.github.com> Date: Fri, 30 Apr 2021 22:44:57 +0200 Subject: [PATCH 4/6] Remove accidental exclamation mark --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 6cf43d82..f825a76c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -81,7 +81,7 @@ android { exclude 'META-INF/licenses/ASM' } - if (!gradle.startParameter.taskNames.any { it.contains("Firebase") }) { + if (gradle.startParameter.taskNames.any { it.contains("Firebase") }) { sourceSets.androidTest.java { srcDirs += ["../feature/about/src/androidTest/java"] srcDirs += ["../feature/users/src/androidTest/java"] From 69ab03b7b058682a2c4050be75c994c9135b11a3 Mon Sep 17 00:00:00 2001 From: Josef Raska <6277721+jraska@users.noreply.github.com> Date: Fri, 30 Apr 2021 23:00:05 +0200 Subject: [PATCH 5/6] Use assets --- .../androidTest/assets/users/two_users.json | 42 ++++++++++++++ .../client/users/test/UsersActivityTest.kt | 56 ++++--------------- 2 files changed, 52 insertions(+), 46 deletions(-) create mode 100644 feature/users/src/androidTest/assets/users/two_users.json diff --git a/feature/users/src/androidTest/assets/users/two_users.json b/feature/users/src/androidTest/assets/users/two_users.json new file mode 100644 index 00000000..ba219835 --- /dev/null +++ b/feature/users/src/androidTest/assets/users/two_users.json @@ -0,0 +1,42 @@ +[ + { + "login": "mojombo", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/mojombo", + "html_url": "https://github.com/mojombo", + "followers_url": "https://api.github.com/users/mojombo/followers", + "following_url": "https://api.github.com/users/mojombo/following{/other_user}", + "gists_url": "https://api.github.com/users/mojombo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/mojombo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/mojombo/subscriptions", + "organizations_url": "https://api.github.com/users/mojombo/orgs", + "repos_url": "https://api.github.com/users/mojombo/repos", + "events_url": "https://api.github.com/users/mojombo/events{/privacy}", + "received_events_url": "https://api.github.com/users/mojombo/received_events", + "type": "User", + "site_admin": false + }, + { + "login": "defunkt", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://avatars0.githubusercontent.com/u/2?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/defunkt", + "html_url": "https://github.com/defunkt", + "followers_url": "https://api.github.com/users/defunkt/followers", + "following_url": "https://api.github.com/users/defunkt/following{/other_user}", + "gists_url": "https://api.github.com/users/defunkt/gists{/gist_id}", + "starred_url": "https://api.github.com/users/defunkt/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/defunkt/subscriptions", + "organizations_url": "https://api.github.com/users/defunkt/orgs", + "repos_url": "https://api.github.com/users/defunkt/repos", + "events_url": "https://api.github.com/users/defunkt/events{/privacy}", + "received_events_url": "https://api.github.com/users/defunkt/received_events", + "type": "User", + "site_admin": true + } +] diff --git a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt index dbdc63d2..08483e4c 100644 --- a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt +++ b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt @@ -3,14 +3,13 @@ package com.jraska.github.client.users.test import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule -import com.jraska.github.client.http.MockWebServerInterceptor import com.jraska.github.client.http.MockWebServerInterceptorRule import com.jraska.github.client.users.ui.UsersActivity import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.assertj.core.api.Assertions.assertThat -import org.junit.Before import org.junit.Rule import org.junit.Test @@ -19,7 +18,8 @@ class UsersActivityTest { @get:Rule val mockWebServer = MockWebServer() - @get:Rule val mockWebServerInterceptorRule = MockWebServerInterceptorRule(mockWebServer) + @get:Rule + val mockWebServerInterceptorRule = MockWebServerInterceptorRule(mockWebServer) @get:Rule val rule = ActivityTestRule(UsersActivity::class.java) @@ -40,47 +40,11 @@ class UsersActivityTest { } } - private fun twoUsersResponse() = MockResponse().setResponseCode(200).setBody( - "[\n" + - " {\n" + - " \"login\": \"mojombo\",\n" + - " \"id\": 1,\n" + - " \"node_id\": \"MDQ6VXNlcjE=\",\n" + - " \"avatar_url\": \"https://avatars.githubusercontent.com/u/1?v=4\",\n" + - " \"gravatar_id\": \"\",\n" + - " \"url\": \"https://api.github.com/users/mojombo\",\n" + - " \"html_url\": \"https://github.com/mojombo\",\n" + - " \"followers_url\": \"https://api.github.com/users/mojombo/followers\",\n" + - " \"following_url\": \"https://api.github.com/users/mojombo/following{/other_user}\",\n" + - " \"gists_url\": \"https://api.github.com/users/mojombo/gists{/gist_id}\",\n" + - " \"starred_url\": \"https://api.github.com/users/mojombo/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\": \"https://api.github.com/users/mojombo/subscriptions\",\n" + - " \"organizations_url\": \"https://api.github.com/users/mojombo/orgs\",\n" + - " \"repos_url\": \"https://api.github.com/users/mojombo/repos\",\n" + - " \"events_url\": \"https://api.github.com/users/mojombo/events{/privacy}\",\n" + - " \"received_events_url\": \"https://api.github.com/users/mojombo/received_events\",\n" + - " \"type\": \"User\",\n" + - " \"site_admin\": false\n" + - " },\n" + - " {\n" + - " \"login\": \"defunkt\",\n" + - " \"id\": 2,\n" + - " \"node_id\": \"MDQ6VXNlcjI=\",\n" + - " \"avatar_url\": \"https://avatars0.githubusercontent.com/u/2?v=4\",\n" + - " \"gravatar_id\": \"\",\n" + - " \"url\": \"https://api.github.com/users/defunkt\",\n" + - " \"html_url\": \"https://github.com/defunkt\",\n" + - " \"followers_url\": \"https://api.github.com/users/defunkt/followers\",\n" + - " \"following_url\": \"https://api.github.com/users/defunkt/following{/other_user}\",\n" + - " \"gists_url\": \"https://api.github.com/users/defunkt/gists{/gist_id}\",\n" + - " \"starred_url\": \"https://api.github.com/users/defunkt/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\": \"https://api.github.com/users/defunkt/subscriptions\",\n" + - " \"organizations_url\": \"https://api.github.com/users/defunkt/orgs\",\n" + - " \"repos_url\": \"https://api.github.com/users/defunkt/repos\",\n" + - " \"events_url\": \"https://api.github.com/users/defunkt/events{/privacy}\",\n" + - " \"received_events_url\": \"https://api.github.com/users/defunkt/received_events\",\n" + - " \"type\": \"User\",\n" + - " \"site_admin\": true\n" + - " }]" - ) + private fun twoUsersResponse(): MockResponse { + val assetsStream = InstrumentationRegistry.getInstrumentation().targetContext.assets.open("users/two_users.json") + + val json = assetsStream.bufferedReader().readText() + + return MockResponse().setResponseCode(200).setBody(json) + } } From 12c029b5962a87259f11fe2cc10cee5ff133d68b Mon Sep 17 00:00:00 2001 From: Josef Raska <6277721+jraska@users.noreply.github.com> Date: Fri, 30 Apr 2021 23:01:32 +0200 Subject: [PATCH 6/6] Use context only --- .../com/jraska/github/client/users/test/UsersActivityTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt index 08483e4c..c9f9921d 100644 --- a/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt +++ b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt @@ -41,7 +41,7 @@ class UsersActivityTest { } private fun twoUsersResponse(): MockResponse { - val assetsStream = InstrumentationRegistry.getInstrumentation().targetContext.assets.open("users/two_users.json") + val assetsStream = InstrumentationRegistry.getInstrumentation().context.assets.open("users/two_users.json") val json = assetsStream.bufferedReader().readText()