diff --git a/app/build.gradle b/app/build.gradle index a46475e8..f825a76c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,6 +86,10 @@ android { 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"] + } } } @@ -157,10 +161,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/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/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..c9f9921d --- /dev/null +++ b/feature/users/src/androidTest/java/com/jraska/github/client/users/test/UsersActivityTest.kt @@ -0,0 +1,50 @@ +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.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.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) + + @Test + fun testLaunches() { + mockWebServer.enqueue(twoUsersResponse()) + + 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 { + val assetsStream = InstrumentationRegistry.getInstrumentation().context.assets.open("users/two_users.json") + + val json = assetsStream.bufferedReader().readText() + + return MockResponse().setResponseCode(200).setBody(json) + } +} 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',