From 163bb8cd10ddc4cc3427517e89d9ce1121b95db7 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Wed, 26 Jan 2022 20:28:43 +0200 Subject: [PATCH 01/17] Issue#13 First part of the Robolectric Documentation --- README.md | 2 +- .../CodeKataFavouriteContentLocalStorage.kt | 51 ++++ .../FavouriteContentLocalStorageImplTest.kt | 26 +- codekata/robolectric.instructionset.md | 237 ++++++++++++++++++ codekata/storage.instructionset | 1 - .../login/CodeKataSecondLoginUseCaseTest.kt | 8 +- 6 files changed, 314 insertions(+), 11 deletions(-) create mode 100644 app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorage.kt rename app/src/robolectricTest/java/org/fnives/test/showcase/{testutils => storage}/favourite/FavouriteContentLocalStorageImplTest.kt (79%) create mode 100644 codekata/robolectric.instructionset.md delete mode 100644 codekata/storage.instructionset diff --git a/README.md b/README.md index c04476c..1655a23 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ We will also see how to test with LiveData. We will introduce Rules, aka easy to reuse "Before" and "After" components. #### App Robolectric Unit Tests. -Open the [app storage unit tests instruction set](./codekata/storage.instructionset). +Open the [app robolectric unit tests instruction set](./codekata/robolectric.instructionset.md). In this section we will see how to test component depending on context such as Room database. diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorage.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorage.kt new file mode 100644 index 0000000..f84bf89 --- /dev/null +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorage.kt @@ -0,0 +1,51 @@ +package org.fnives.test.showcase.storage.favourite + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Disabled + +@Disabled("CodeKata") +@OptIn(ExperimentalCoroutinesApi::class) +class CodeKataFavouriteContentLocalStorage { + + @Before + fun setUp() { + } + + @After + fun tearDown() { + } + + /** GIVEN just created database WHEN querying THEN empty list is returned */ + @Test + fun atTheStartOurDatabaseIsEmpty() = runBlocking { + } + + /** GIVEN content_id WHEN added to Favourite THEN it can be read out */ + @Test + fun addingContentIdToFavouriteCanBeLaterReadOut() { + } + + /** GIVEN content_id added WHEN removed to Favourite THEN it no longer can be read out */ + @Test + fun contentIdAddedThenRemovedCanNoLongerBeReadOut() { + } + + /** GIVEN empty database WHILE observing content WHEN favourite added THEN change is emitted */ + @Test + fun addingFavouriteUpdatesExistingObservers() { + } + + /** GIVEN non empty database WHILE observing content WHEN favourite removed THEN change is emitted */ + @Test + fun removingFavouriteUpdatesExistingObservers() { + } + + /** GIVEN an observed WHEN adding and removing from it THEN we only get the expected amount of updates */ + @Test + fun noUnexpectedUpdates() { + } +} diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/favourite/FavouriteContentLocalStorageImplTest.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplTest.kt similarity index 79% rename from app/src/robolectricTest/java/org/fnives/test/showcase/testutils/favourite/FavouriteContentLocalStorageImplTest.kt rename to app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplTest.kt index 2c77b78..479e5f6 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/favourite/FavouriteContentLocalStorageImplTest.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplTest.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.favourite +package org.fnives.test.showcase.storage.favourite import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,6 +42,14 @@ internal class FavouriteContentLocalStorageImplTest : KoinTest { stopKoin() } + /** GIVEN just created database WHEN querying THEN empty list is returned */ + @Test + fun atTheStartOurDatabaseIsEmpty() = runTest(testDispatcher) { + val actual = sut.observeFavourites().first() + + Assert.assertEquals(emptyList(), actual) + } + /** GIVEN content_id WHEN added to Favourite THEN it can be read out */ @Test fun addingContentIdToFavouriteCanBeLaterReadOut() = runTest(testDispatcher) { @@ -69,7 +77,6 @@ internal class FavouriteContentLocalStorageImplTest : KoinTest { @Test fun addingFavouriteUpdatesExistingObservers() = runTest(testDispatcher) { val expected = listOf(listOf(), listOf(ContentId("a"))) - val actual = async(coroutineContext) { sut.observeFavourites().take(2).toList() } advanceUntilIdle() @@ -95,4 +102,19 @@ internal class FavouriteContentLocalStorageImplTest : KoinTest { Assert.assertEquals(expected, actual.getCompleted()) } + + /** GIVEN an observed WHEN adding and removing from it THEN we only get the expected amount of updates */ + @Test + fun noUnexpectedUpdates() = runTest(testDispatcher) { + val actual = async(coroutineContext) { sut.observeFavourites().take(4).toList() } + advanceUntilIdle() + + sut.markAsFavourite(ContentId("a")) + advanceUntilIdle() + sut.deleteAsFavourite(ContentId("a")) + advanceUntilIdle() + + Assert.assertFalse(actual.isCompleted) + actual.cancel() + } } diff --git a/codekata/robolectric.instructionset.md b/codekata/robolectric.instructionset.md new file mode 100644 index 0000000..3c3aab5 --- /dev/null +++ b/codekata/robolectric.instructionset.md @@ -0,0 +1,237 @@ +# 3. Starting of Robolectric testing + +So we are finally here, so far we didn't had to touch any kind of context or resources, activities, fragments or anything android. This is where we have to get back to reality and actually deal with Android. + +In this testing instruction set you will learn how to write simple tests using Robolectric. + +- We will learn why Robolectric is useful +- Learn how to test Room daos +- Learn how to test Room Migrations +- Learn what a Robolectric Shadow is +- And Learn how to write basic UI tests + +## FavouriteContentLocalStorage test + +Our System Under Test will be `org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage` or more precisely it's implementation: `org.fnives.test.showcase.storage.favourite.FavouriteContentLocalStorageImpl` + +What it does is: +- it's an abstraction over the Room DAO +- has 3 methods: observe, add and delete +- it gets the data from Room and updates Room + +### Setup + +So let's start with the setup. + +Our test class is `org.fnives.test.showcase.storage.favourite.CodeKataFavouriteContentLocalStorage` + +Question: Why don't we test the DAO and Storage separately using mocking? +Answer: The same logic applies how we didn't test the RetrofitServices just the RemoteSources. The Service just like the DAO is an implementation detail, our code only accesses them through the RemoteSource / LocalStorage abstraction. With this in mind now we only want to test that we interact with the database properly, we don't really care how many DAOs are used. + +We don't add anything Robolectric just yet, let's try to do this without it first. + +Let's setup or System Under Test as usual: + +```kotlin +private lateinit var sut: FavouriteContentLocalStorage // notice we only care about the interface + +@Before +fun setup() { + val room = Room.inMemoryDatabaseBuilder(mock(), LocalDatabase::class.java) // we are using inmemory, cause we don't really want to create files. + .allowMainThreadQueries() // we don't really care about threading for now + .build() + + sut = FavouriteContentLocalStorageImpl(room.favouriteDao) +} + +@Test +fun atTheStartOurDatabaseIsEmpty() = runBlocking { + // we just verify our setup is correct + sut.observeFavourites().first() +} +``` + +Let's run our test and see: + +> Method getWritableDatabase in android.database.sqlite.SQLiteOpenHelper not mocked. See http://g.co/androidstudio/not-mocked for details. +> java.lang.RuntimeException: Method getWritableDatabase in android.database.sqlite.SQLiteOpenHelper not mocked. See http://g.co/androidstudio/not-mocked for details. +> at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java) + + +So we need to mock something inside the `SQLiteOpenHelper` which is used inside the Dao and Room in order to test the Database. +Well, I would rather not do that. So then we need to test on a Real Device or Emulator. Well we could, but then we need to integrate a Testing Farm with our CI. It would be good to do that, but sometimes that's just not possible, here is where [Robolectric](http://robolectric.org/) comes in. + +>Robolectric is the industry-standard unit testing framework for Android. With Robolectric, your tests run in a simulated Android environment inside a JVM, without the overhead and flakiness of an emulator. Robolectric tests routinely run 10x faster than those on cold-started emulators. + +### Setup with Robolectric + +We already have the dependencies in the project. +We need to annotate our class with `@RunWith(AndroidJUnit4::class)` +With this Robolectric actually starts our `TestShowcaseApplication` so instead of creating our SUT, we just inject it. However to easily inject with Koin, we extend `KoinTest`: +```kotlin +@RunWith(AndroidJUnit4::class) +class CodeKataFavouriteContentLocalStorage: KoinTest +``` + +So additional changes will be: +- remove our previous mocking attempt +- we inject our SUT +- we stop koin in tearDown +- we add a testDispatcher to Room +- we switch to runTest(testDispatcher) + +Since Room has their own exercutors, that could make our tests flaky, since we might get out of sync. Luckily we can switch out these executors, so we do that to make sure our tests run just as we would like them to. + +``` +private val sut by inject() +private lateinit var testDispatcher: TestDispatcher + +@Before +fun setUp() { + testDispatcher = StandardTestDispatcher(TestCoroutineScheduler()) + DatabaseInitialization.dispatcher = testDispatcher +} + +@After +fun tearDown() { + stopKoin() +} + +@Test +fun atTheStartOurDatabaseIsEmpty()= runTest(testDispatcher) { + sut.observeFavourites().first() +} +``` + +The line `DatabaseInitialization.dispatcher = testDispatcher` may look a bit mysterious, but all we do her is overwrite our iriginal DatabaseInitialization in tests, and use the given Dispatcher as an executor for Room setup. + +Now if we run our test we see we can indeed access the database. We can get down to actual testing. + +### 1. `atTheStartOurDatabaseIsEmpty` + +Since we used this test for our setup, we just need to finish it. We just verify the returned list is empty, so: +```kotlin +@Test +fun atTheStartOurDatabaseIsEmpty() = runTest(testDispatcher) { + val actual = sut.observeFavourites().first() + + Assert.assertEquals(emptyList(), actual) + // note we are using Assert instead of Assertions, that's because Robolectric and AndroidTest support JUnit4 and not JUnit5 we used previously. The @Test @Before etc annotations are also different. +} +``` + +### 2. `addingContentIdToFavouriteCanBeLaterReadOut` + +Time to test some actual logic. Let's see if we add an element to the Database, we indead can query it back. +First we declare what we expect: +```kotlin +val expected = listOf(ContentId("a")) +``` + +We do the action: +```kotlin +sut.markAsFavourite(ContentId("a")) +val actual = sut.observeFavourites().first() +``` + +And at the end verify: +```kotlin +Assert.assertEquals(expected, actual) +``` + +It is as simple as that. + +### 3. `contentIdAddedThenRemovedCanNoLongerBeReadOut` + +So we can add to the Database, let's see if we can remove from it. +We expect nothing, and we add an element as a setup: +```kotlin +val expected = listOf() +sut.markAsFavourite(ContentId("b")) +``` + +We do the action: +```kotlin +sut.deleteAsFavourite(ContentId("b")) +val actual = sut.observeFavourites().first() +``` + +And just verify our expectation: +```kotlin +Assert.assertEquals(expected, actual) +``` + +So we can delete as well. + +### 4. `addingFavouriteUpdatesExistingObservers` +Until now we just verified that afterwards we get the correct data, but what if we already subscribed? Do we still get the correct updates? + +So we setup our expectations and our observer: +```kotlin +val expected = listOf(listOf(), listOf(ContentId("observe"))) +val actual = async(coroutineContext) { sut.observeFavourites().take(2).toList() } +advanceUntilIdle() // we sync, so we get the first element that is in the database (which is the emptyList). +``` + +Now we do the action and synchronize again, so our observer is potentially updated: +```kotlin +sut.markAsFavourite(ContentId("a")) +advanceUntilIdle() +``` + +And let's assert that indeed we only get these two updates and no more things happening. To do this we won't wait for the async, but just get it's Completed value, aka ensure it is finished. + +```kotlin +Assert.assertEquals(expected, actual.getCompleted()) +``` + +##### Note: we can use turbine as well to verify our flows, just like we did previously + +### 5. `removingFavouriteUpdatesExistingObservers` + +Okay, this should be really similar to `addingFavouriteUpdatesExistingObservers` just with a hint of `contentIdAddedThenRemovedCanNoLongerBeReadOut` so try to write it on your own. + +However for completness sake: +```kotlin +val expected = listOf(listOf(ContentId("a")), listOf()) +sut.markAsFavourite(ContentId("a")) + +val actual = async(coroutineContext) { + sut.observeFavourites().take(2).toList() +} +advanceUntilIdle() + +sut.deleteAsFavourite(ContentId("a")) +advanceUntilIdle() + +Assert.assertEquals(expected, actual.getCompleted()) +``` + +### 6.`noUnexpectedUpdates` +Until now, just like with Flow tests in core, we assumed the number of updates. +So it's time to verify that we don't get unexpected updates on our flow. + +To do this we don't really care about the results, just that the number of updates are correct. So let's observe the database with the Correct Update Count + 1. +```kotlin +val actual = async(coroutineContext) { sut.observeFavourites().take(4).toList() } +advanceUntilIdle() // we expect to get our first result with emptyList() +``` + +We modify the database: +```kotlin +sut.markAsFavourite(ContentId("a")) +advanceUntilIdle() // we expect to get our second update with added ContentID +sut.deleteAsFavourite(ContentId("a")) +advanceUntilIdle() // we expect to get our third update with emptyList again +``` + +And now we verify that the observation did not complete, aka no 4th update was received: +```kotlin +Assert.assertFalse(actual.isCompleted) +actual.cancel() +``` + +With that we know how to verify our Database running on the JVM, without needing an emulator or device. + +## Conclusion + diff --git a/codekata/storage.instructionset b/codekata/storage.instructionset deleted file mode 100644 index 30404ce..0000000 --- a/codekata/storage.instructionset +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file diff --git a/core/src/test/java/org/fnives/test/showcase/core/login/CodeKataSecondLoginUseCaseTest.kt b/core/src/test/java/org/fnives/test/showcase/core/login/CodeKataSecondLoginUseCaseTest.kt index a689882..95bf4f5 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/login/CodeKataSecondLoginUseCaseTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/login/CodeKataSecondLoginUseCaseTest.kt @@ -10,36 +10,30 @@ class CodeKataSecondLoginUseCaseTest { @BeforeEach fun setUp() { - TODO() } @DisplayName("GIVEN empty username WHEN trying to login THEN invalid username is returned") @Test fun emptyUserNameReturnsLoginStatusError() { - TODO() } @DisplayName("GIVEN empty password WHEN trying to login THEN invalid password is returned") @Test fun emptyPasswordNameReturnsLoginStatusError() { - TODO() } @DisplayName("GIVEN invalid credentials response WHEN trying to login THEN invalid credentials is returned ") @Test fun invalidLoginResponseReturnInvalidCredentials() { - TODO() } @DisplayName("GIVEN success response WHEN trying to login THEN session is saved and success is returned") @Test fun validResponseResultsInSavingSessionAndSuccessReturned() { - TODO() } - @DisplayName("GIVEN error resposne WHEN trying to login THEN session is not touched and error is returned") + @DisplayName("GIVEN error response WHEN trying to login THEN session is not touched and error is returned") @Test fun invalidResponseResultsInErrorReturned() { - TODO() } } From 8d33f77cec99ad0d4a36e07662d65d45be5726eb Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Wed, 26 Jan 2022 22:33:40 +0200 Subject: [PATCH 02/17] Issue#13 Remove BaseUrlProvider --- .../test/showcase/TestShowcaseApplication.kt | 5 ++-- .../test/showcase/di/BaseUrlProvider.kt | 9 ------- .../test/showcase/di/BaseUrlProvider.kt | 9 ------- .../ReloadKoinModulesIfNecessaryTestRule.kt | 24 +++++++------------ 4 files changed, 12 insertions(+), 35 deletions(-) delete mode 100644 app/src/main/java/org/fnives/test/showcase/di/BaseUrlProvider.kt delete mode 100644 app/src/sharedTest/java/org/fnives/test/showcase/di/BaseUrlProvider.kt diff --git a/app/src/main/java/org/fnives/test/showcase/TestShowcaseApplication.kt b/app/src/main/java/org/fnives/test/showcase/TestShowcaseApplication.kt index 60b05b4..5e63e64 100644 --- a/app/src/main/java/org/fnives/test/showcase/TestShowcaseApplication.kt +++ b/app/src/main/java/org/fnives/test/showcase/TestShowcaseApplication.kt @@ -1,8 +1,8 @@ package org.fnives.test.showcase import android.app.Application -import org.fnives.test.showcase.di.BaseUrlProvider import org.fnives.test.showcase.di.createAppModules +import org.fnives.test.showcase.model.network.BaseUrl import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @@ -10,9 +10,10 @@ class TestShowcaseApplication : Application() { override fun onCreate() { super.onCreate() + val baseUrl = BaseUrl(BuildConfig.BASE_URL) startKoin { androidContext(this@TestShowcaseApplication) - modules(createAppModules(BaseUrlProvider.get())) + modules(createAppModules(baseUrl)) } } } diff --git a/app/src/main/java/org/fnives/test/showcase/di/BaseUrlProvider.kt b/app/src/main/java/org/fnives/test/showcase/di/BaseUrlProvider.kt deleted file mode 100644 index 9b5e5ba..0000000 --- a/app/src/main/java/org/fnives/test/showcase/di/BaseUrlProvider.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.fnives.test.showcase.di - -import org.fnives.test.showcase.BuildConfig -import org.fnives.test.showcase.model.network.BaseUrl - -object BaseUrlProvider { - - fun get() = BaseUrl(BuildConfig.BASE_URL) -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/di/BaseUrlProvider.kt b/app/src/sharedTest/java/org/fnives/test/showcase/di/BaseUrlProvider.kt deleted file mode 100644 index 4be8d81..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/di/BaseUrlProvider.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.fnives.test.showcase.di - -import org.fnives.test.showcase.model.network.BaseUrl -import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory - -object BaseUrlProvider { - - fun get() = BaseUrl(SpecificTestConfigurationsFactory.createServerTypeConfiguration().url) -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt index 50b1427..b3d7c6b 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt @@ -1,29 +1,23 @@ package org.fnives.test.showcase.testutils -import androidx.test.core.app.ApplicationProvider -import org.fnives.test.showcase.TestShowcaseApplication -import org.fnives.test.showcase.di.BaseUrlProvider -import org.fnives.test.showcase.di.createAppModules import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import org.koin.android.ext.koin.androidContext -import org.koin.core.context.GlobalContext -import org.koin.core.context.startKoin import org.koin.core.context.stopKoin class ReloadKoinModulesIfNecessaryTestRule : TestRule { override fun apply(base: Statement, description: Description): Statement = object : Statement() { override fun evaluate() { - if (GlobalContext.getOrNull() == null) { - val application = - ApplicationProvider.getApplicationContext() - startKoin { - androidContext(application) - modules(createAppModules(BaseUrlProvider.get())) - } - } + // TODO +// if (GlobalContext.getOrNull() == null) { +// val application = +// ApplicationProvider.getApplicationContext() +// startKoin { +// androidContext(application) +// modules(createAppModules(BaseUrlProvider.get())) +// } +// } try { base.evaluate() } finally { From 9bdcaddb0c678358ae9f03325d71ae89069cbdd9 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Wed, 26 Jan 2022 22:35:27 +0200 Subject: [PATCH 03/17] Issue#13 Use any available port for mockwebserver in core tests --- .../showcase/network/mockserver/MockServerScenarioSetup.kt | 7 ++----- .../network/shared/MockServerScenarioSetupExtensions.kt | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/MockServerScenarioSetup.kt b/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/MockServerScenarioSetup.kt index b1da9c2..f1a94f4 100644 --- a/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/MockServerScenarioSetup.kt +++ b/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/MockServerScenarioSetup.kt @@ -20,13 +20,13 @@ class MockServerScenarioSetup internal constructor( var clientCertificates: HandshakeCertificates? = null private set - fun start(useHttps: Boolean) { + fun start(useHttps: Boolean): String { val mockWebServer = MockWebServer().also { this.mockWebServer = it } if (useHttps) { clientCertificates = mockWebServer.useHttps() } mockWebServer.dispatcher = networkDispatcher - mockWebServer.start(InetAddress.getLocalHost(), PORT) + return mockWebServer.url("/").toString() } /** @@ -69,9 +69,6 @@ class MockServerScenarioSetup internal constructor( } companion object { - const val PORT: Int = 7335 - val HTTP_BASE_URL get() = "http://${InetAddress.getLocalHost().canonicalHostName}" - val HTTPS_BASE_URL get() = "https://localhost" private fun MockWebServer.useHttps(): HandshakeCertificates { val localhost = InetAddress.getByName("localhost").canonicalHostName diff --git a/network/src/test/java/org/fnives/test/showcase/network/shared/MockServerScenarioSetupExtensions.kt b/network/src/test/java/org/fnives/test/showcase/network/shared/MockServerScenarioSetupExtensions.kt index a74864d..6c6fd7d 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/shared/MockServerScenarioSetupExtensions.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/shared/MockServerScenarioSetupExtensions.kt @@ -7,12 +7,12 @@ import org.junit.jupiter.api.extension.ExtensionContext class MockServerScenarioSetupExtensions : BeforeEachCallback, AfterEachCallback { - val url: String = "${MockServerScenarioSetup.HTTP_BASE_URL}:${MockServerScenarioSetup.PORT}/" + lateinit var url: String lateinit var mockServerScenarioSetup: MockServerScenarioSetup override fun beforeEach(context: ExtensionContext?) { mockServerScenarioSetup = MockServerScenarioSetup() - mockServerScenarioSetup.start(false) + url = mockServerScenarioSetup.start(false) } override fun afterEach(context: ExtensionContext?) { From b27846609544e35c91dc3d90867ca8e19d2d5654 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Wed, 26 Jan 2022 23:34:58 +0200 Subject: [PATCH 04/17] Issue#13 Simplify swapping of URL and certificate for tests --- app/build.gradle | 2 +- .../AndroidTestServerTypeConfiguration.kt | 29 --------- .../SpecificTestConfigurationsFactory.kt | 3 - .../RobolectricServerTypeConfiguration.kt | 11 ---- .../SpecificTestConfigurationsFactory.kt | 3 - .../MockServerScenarioSetupTestRule.kt | 60 ++++++++++++++----- .../ReloadKoinModulesIfNecessaryTestRule.kt | 56 +++++++++++------ .../TestConfigurationsFactory.kt | 2 - .../idling/NetworkSynchronization.kt | 6 +- .../test/showcase/ui/home/MainActivityTest.kt | 7 --- .../showcase/ui/login/AuthActivityTest.kt | 7 --- .../showcase/ui/splash/SplashActivityTest.kt | 7 --- .../network/di/createNetworkmodules.kt | 11 +++- 13 files changed, 96 insertions(+), 108 deletions(-) delete mode 100644 app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt delete mode 100644 app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricServerTypeConfiguration.kt diff --git a/app/build.gradle b/app/build.gradle index 163f056..8bfc52f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,7 +38,7 @@ android { sourceSets { androidTest { - java.srcDirs += "src/sharedTest/java" +// java.srcDirs += "src/sharedTest/java" assets.srcDirs += files("$projectDir/schemas".toString()) } test { diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt deleted file mode 100644 index 319668e..0000000 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -import okhttp3.OkHttpClient -import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup -import org.fnives.test.showcase.testutils.idling.NetworkSynchronization -import org.koin.core.context.loadKoinModules -import org.koin.core.qualifier.StringQualifier -import org.koin.dsl.module -import org.koin.test.KoinTest -import org.koin.test.get - -object AndroidTestServerTypeConfiguration : ServerTypeConfiguration, KoinTest { - override val useHttps: Boolean get() = true - - override val url: String get() = "${MockServerScenarioSetup.HTTPS_BASE_URL}:${MockServerScenarioSetup.PORT}/" - - override fun invoke(mockServerScenarioSetup: MockServerScenarioSetup) { - val handshakeCertificates = mockServerScenarioSetup.clientCertificates ?: return - val sessionless = StringQualifier(NetworkSynchronization.OkHttpClientTypes.SESSIONLESS.qualifier) - val okHttpClientWithCertificate = get(sessionless).newBuilder() - .sslSocketFactory(handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager) - .build() - loadKoinModules( - module { - single(qualifier = sessionless) { okHttpClientWithCertificate } - } - ) - } -} diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt index 90680c0..6a3da36 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt @@ -4,9 +4,6 @@ object SpecificTestConfigurationsFactory : TestConfigurationsFactory { override fun createMainDispatcherTestRule(): MainDispatcherTestRule = AndroidTestMainDispatcherTestRule() - override fun createServerTypeConfiguration(): ServerTypeConfiguration = - AndroidTestServerTypeConfiguration - override fun createLoginRobotConfiguration(): LoginRobotConfiguration = AndroidTestLoginRobotConfiguration diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricServerTypeConfiguration.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricServerTypeConfiguration.kt deleted file mode 100644 index 895f466..0000000 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricServerTypeConfiguration.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup - -object RobolectricServerTypeConfiguration : ServerTypeConfiguration { - override val useHttps: Boolean = false - - override val url: String get() = "${MockServerScenarioSetup.HTTP_BASE_URL}:${MockServerScenarioSetup.PORT}/" - - override fun invoke(mockServerScenarioSetup: MockServerScenarioSetup) = Unit -} diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt index 5028a4b..ff7e7af 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt @@ -4,9 +4,6 @@ object SpecificTestConfigurationsFactory : TestConfigurationsFactory { override fun createMainDispatcherTestRule(): MainDispatcherTestRule = TestCoroutineMainDispatcherTestRule() - override fun createServerTypeConfiguration(): ServerTypeConfiguration = - RobolectricServerTypeConfiguration - override fun createLoginRobotConfiguration(): LoginRobotConfiguration = RobolectricLoginRobotConfiguration diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupTestRule.kt index df076ab..36755f0 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupTestRule.kt @@ -1,35 +1,63 @@ package org.fnives.test.showcase.testutils +import okhttp3.OkHttpClient +import okhttp3.tls.HandshakeCertificates +import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup -import org.fnives.test.showcase.testutils.configuration.ServerTypeConfiguration -import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory -import org.junit.rules.TestRule +import org.fnives.test.showcase.testutils.idling.NetworkSynchronization.OkHttpClientTypes import org.junit.runner.Description import org.junit.runners.model.Statement +import org.koin.core.context.loadKoinModules +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.get + +class MockServerScenarioSetupTestRule : ReloadKoinModulesIfNecessaryTestRule(), KoinTest { -class MockServerScenarioSetupTestRule( - val serverTypeConfiguration: ServerTypeConfiguration = SpecificTestConfigurationsFactory.createServerTypeConfiguration() -) : TestRule { lateinit var mockServerScenarioSetup: MockServerScenarioSetup + private val sessionlessQualifier get() = OkHttpClientTypes.SESSIONLESS.asQualifier() + override fun apply(base: Statement, description: Description): Statement = - object : Statement() { - @Throws(Throwable::class) - override fun evaluate() { - before() - try { - base.evaluate() - } finally { - after() - } + super.apply(createStatement(base), description) + + private fun createStatement(base: Statement) = object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + before() + try { + base.evaluate() + } finally { + after() } } + } private fun before() { mockServerScenarioSetup = MockServerScenarioSetup() - mockServerScenarioSetup.start(serverTypeConfiguration.useHttps) + val url = mockServerScenarioSetup.start(true) + + val handshakeCertificates = mockServerScenarioSetup.clientCertificates + ?: throw IllegalStateException("ClientCertificate should be accessable") + + val okHttpClientWithCertificate = createUpdateOkHttpClient(handshakeCertificates) + + loadKoinModules( + module { + // add https certificate to okhttp + single(qualifier = sessionlessQualifier) { okHttpClientWithCertificate } + // replace base url with mockWebServer's + single { BaseUrl(url) } + } + ) } + private fun createUpdateOkHttpClient(handshakeCertificates: HandshakeCertificates) = + get(sessionlessQualifier).newBuilder() + .sslSocketFactory(handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager) + .build() + + private fun after() { mockServerScenarioSetup.stop() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt index b3d7c6b..e241826 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt @@ -1,28 +1,50 @@ package org.fnives.test.showcase.testutils +import androidx.test.core.app.ApplicationProvider +import org.fnives.test.showcase.BuildConfig +import org.fnives.test.showcase.TestShowcaseApplication +import org.fnives.test.showcase.di.createAppModules +import org.fnives.test.showcase.model.network.BaseUrl import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin import org.koin.core.context.stopKoin +import org.koin.mp.KoinPlatformTools +import org.koin.test.KoinTest -class ReloadKoinModulesIfNecessaryTestRule : TestRule { +/** + * Test rule to help reinitialize the whole Koin setup. + * + * It's needed because in AndroidTest's the Application is only called once, + * meaning our koin would be shared. + * + * Note: Do not use if you want your test's to share Koin, and in such case do not stop your Koin. + */ +open class ReloadKoinModulesIfNecessaryTestRule : TestRule, KoinTest { override fun apply(base: Statement, description: Description): Statement = - object : Statement() { - override fun evaluate() { - // TODO -// if (GlobalContext.getOrNull() == null) { -// val application = -// ApplicationProvider.getApplicationContext() -// startKoin { -// androidContext(application) -// modules(createAppModules(BaseUrlProvider.get())) -// } -// } - try { - base.evaluate() - } finally { - stopKoin() - } + ReinitKoinStatement(base) + + class ReinitKoinStatement(private val base: Statement) : Statement() { + override fun evaluate() { + reinitKoinIfNeeded() + try { + base.evaluate() + } finally { + stopKoin() } } + + private fun reinitKoinIfNeeded() { + if (KoinPlatformTools.defaultContext().getOrNull() != null) return + + val application = ApplicationProvider.getApplicationContext() + val baseUrl = BaseUrl(BuildConfig.BASE_URL) + startKoin { + androidContext(application) + modules(createAppModules(baseUrl)) + } + } + } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt index ed406a1..3ac0428 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt @@ -10,8 +10,6 @@ interface TestConfigurationsFactory { fun createMainDispatcherTestRule(): MainDispatcherTestRule - fun createServerTypeConfiguration(): ServerTypeConfiguration - fun createLoginRobotConfiguration(): LoginRobotConfiguration fun createSnackbarVerification(): SnackbarVerificationTestRule diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt index 7bcb095..135dce1 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt @@ -21,12 +21,14 @@ object NetworkSynchronization : KoinTest { return CompositeDisposable(idlingResources) } - private fun getOkHttpClient(type: OkHttpClientTypes): OkHttpClient = get(StringQualifier(type.qualifier)) + private fun getOkHttpClient(type: OkHttpClientTypes): OkHttpClient = get(type.asQualifier()) private fun OkHttpClient.asIdlingResource(name: String): IdlingResource = OkHttp3IdlingResource.create(name, this) enum class OkHttpClientTypes(val qualifier: String) { - SESSION("SESSION-NETWORKING"), SESSIONLESS("SESSIONLESS-NETWORKING") + SESSION("SESSION-NETWORKING"), SESSIONLESS("SESSIONLESS-NETWORKING"); + + fun asQualifier() = StringQualifier(qualifier) } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt index e4ff3a2..0607d99 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt @@ -8,7 +8,6 @@ import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule -import org.fnives.test.showcase.testutils.ReloadKoinModulesIfNecessaryTestRule import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory import org.fnives.test.showcase.testutils.idling.Disposable import org.fnives.test.showcase.testutils.idling.NetworkSynchronization @@ -52,16 +51,10 @@ class MainActivityTest : KoinTest { @JvmField val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule() - @Rule - @JvmField - val reloadKoinModulesIfNecessaryTestRule = ReloadKoinModulesIfNecessaryTestRule() - private lateinit var disposable: Disposable @Before fun setUp() { - SpecificTestConfigurationsFactory.createServerTypeConfiguration() - .invoke(mockServerScenarioSetup) disposable = NetworkSynchronization.registerNetworkingSynchronization() homeRobot.setupLogin( diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt index f725e74..e81b091 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt @@ -6,7 +6,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule -import org.fnives.test.showcase.testutils.ReloadKoinModulesIfNecessaryTestRule import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory import org.fnives.test.showcase.testutils.idling.Disposable import org.fnives.test.showcase.testutils.idling.NetworkSynchronization @@ -47,16 +46,10 @@ class AuthActivityTest : KoinTest { @JvmField val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule() - @Rule - @JvmField - val reloadKoinModulesIfNecessaryTestRule = ReloadKoinModulesIfNecessaryTestRule() - private lateinit var disposable: Disposable @Before fun setUp() { - SpecificTestConfigurationsFactory.createServerTypeConfiguration() - .invoke(mockServerScenarioSetup) disposable = NetworkSynchronization.registerNetworkingSynchronization() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt index ac904e2..71fabde 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule -import org.fnives.test.showcase.testutils.ReloadKoinModulesIfNecessaryTestRule import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory import org.fnives.test.showcase.testutils.idling.Disposable import org.fnives.test.showcase.testutils.idling.NetworkSynchronization @@ -36,16 +35,10 @@ class SplashActivityTest : KoinTest { @JvmField val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule() - @Rule - @JvmField - val reloadKoinModulesIfNecessaryTestRule = ReloadKoinModulesIfNecessaryTestRule() - lateinit var disposable: Disposable @Before fun setUp() { - SpecificTestConfigurationsFactory.createServerTypeConfiguration() - .invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup) disposable = NetworkSynchronization.registerNetworkingSynchronization() } diff --git a/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt b/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt index b4b0e8b..019faa7 100644 --- a/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt +++ b/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt @@ -29,12 +29,17 @@ fun createNetworkModules( networkSessionExpirationListenerProvider: Scope.() -> NetworkSessionExpirationListener ): Sequence = sequenceOf( + baseUrlModule(baseUrl), loginModule(), contentModule(), - sessionlessNetworkingModule(baseUrl, enableLogging), + sessionlessNetworkingModule(enableLogging), sessionNetworkingModule(networkSessionLocalStorageProvider, networkSessionExpirationListenerProvider) ) +private fun baseUrlModule(baseUrl: BaseUrl) = module { + single { baseUrl } +} + private fun loginModule() = module { factory { LoginRemoteSourceImpl(get(), get()) } factory { get() } @@ -48,7 +53,7 @@ private fun contentModule() = module { factory { get() } } -private fun sessionlessNetworkingModule(baseUrl: BaseUrl, enableLogging: Boolean) = module { +private fun sessionlessNetworkingModule(enableLogging: Boolean) = module { factory { MoshiConverterFactory.create() } single(qualifier = sessionless) { OkHttpClient.Builder() @@ -58,7 +63,7 @@ private fun sessionlessNetworkingModule(baseUrl: BaseUrl, enableLogging: Boolean } single(qualifier = sessionless) { Retrofit.Builder() - .baseUrl(baseUrl.baseUrl) + .baseUrl(get().baseUrl) .addConverterFactory(get()) .client(get(sessionless)) .build() From d8b9fadcbc09a99c73655eafa7eec6770b3dbfbd Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Wed, 26 Jan 2022 23:38:34 +0200 Subject: [PATCH 05/17] Issue#13 Remove unnecessary Rules --- app/build.gradle | 2 +- .../fnives/test/showcase/ui/home/MainActivityTest.kt | 10 ---------- .../fnives/test/showcase/ui/login/AuthActivityTest.kt | 9 --------- 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8bfc52f..163f056 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,7 +38,7 @@ android { sourceSets { androidTest { -// java.srcDirs += "src/sharedTest/java" + java.srcDirs += "src/sharedTest/java" assets.srcDirs += files("$projectDir/schemas".toString()) } test { diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt index 0607d99..269d9c5 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt @@ -1,6 +1,5 @@ package org.fnives.test.showcase.ui.home -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.model.content.FavouriteContent @@ -27,15 +26,6 @@ class MainActivityTest : KoinTest { private lateinit var activityScenario: ActivityScenario - @Rule - @JvmField - val instantTaskExecutorRule = InstantTaskExecutorRule() - - @Rule - @JvmField - val snackbarVerificationTestRule = - SpecificTestConfigurationsFactory.createSnackbarVerification() - @Rule @JvmField val robotRule = RobotTestRule(HomeRobot()) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt index e81b091..26656c6 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt @@ -1,6 +1,5 @@ package org.fnives.test.showcase.ui.login -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R @@ -24,14 +23,6 @@ class AuthActivityTest : KoinTest { private lateinit var activityScenario: ActivityScenario - @Rule - @JvmField - val instantTaskExecutorRule = InstantTaskExecutorRule() - - @Rule - @JvmField - val snackbarVerificationTestRule = SpecificTestConfigurationsFactory.createSnackbarVerification() - @Rule @JvmField val robotRule = RobotTestRule(LoginRobot()) From 5a08525e42fb1336d176614896b64de7bd8407c9 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 00:09:36 +0200 Subject: [PATCH 06/17] Issue#13 Simplify rule sets to minimal and appy them in order --- ...ockServerScenarioSetupResetingTestRule.kt} | 20 ++++- .../ReloadKoinModulesIfNecessaryTestRule.kt | 4 +- .../configuration/MainDispatcherTestRule.kt | 4 + ...n.kt => NetworkSynchronizationTestRule.kt} | 25 +++++- .../statesetup/SetupAuthenticationState.kt | 21 +++-- .../fnives/test/showcase/ui/home/HomeRobot.kt | 15 +--- .../test/showcase/ui/home/MainActivityTest.kt | 79 ++++++++----------- .../showcase/ui/login/AuthActivityTest.kt | 53 +++++-------- .../showcase/ui/splash/SplashActivityTest.kt | 51 +++++------- .../test/showcase/ui/splash/SplashRobot.kt | 25 ------ 10 files changed, 140 insertions(+), 157 deletions(-) rename app/src/sharedTest/java/org/fnives/test/showcase/testutils/{MockServerScenarioSetupTestRule.kt => MockServerScenarioSetupResetingTestRule.kt} (69%) rename app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/{NetworkSynchronization.kt => NetworkSynchronizationTestRule.kt} (59%) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt similarity index 69% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupTestRule.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt index 36755f0..cd1c405 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt @@ -4,7 +4,9 @@ import okhttp3.OkHttpClient import okhttp3.tls.HandshakeCertificates import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup -import org.fnives.test.showcase.testutils.idling.NetworkSynchronization.OkHttpClientTypes +import org.fnives.test.showcase.testutils.idling.NetworkSynchronizationTestRule +import org.fnives.test.showcase.testutils.idling.NetworkSynchronizationTestRule.OkHttpClientTypes +import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement import org.koin.core.context.loadKoinModules @@ -12,14 +14,26 @@ import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.get -class MockServerScenarioSetupTestRule : ReloadKoinModulesIfNecessaryTestRule(), KoinTest { +/** + * TestRule which ensures Koin is reseted between each tests and setups Network mocking. + * + * It First resets koin if needed. + * Then creates and starts the mockwebserver, it also injects the correct baseUrl into the OkHttp Client. + * Then synchronizes Espresso with the OkHttp Client + */ +class MockServerScenarioSetupResetingTestRule( + private val reloadKoinModulesIfNecessaryTestRule: ReloadKoinModulesIfNecessaryTestRule = ReloadKoinModulesIfNecessaryTestRule(), + private val networkSynchronizationTestRule: NetworkSynchronizationTestRule = NetworkSynchronizationTestRule() +) : TestRule, KoinTest { lateinit var mockServerScenarioSetup: MockServerScenarioSetup private val sessionlessQualifier get() = OkHttpClientTypes.SESSIONLESS.asQualifier() override fun apply(base: Statement, description: Description): Statement = - super.apply(createStatement(base), description) + networkSynchronizationTestRule.apply(base, description) + .let(::createStatement) + .let { reloadKoinModulesIfNecessaryTestRule.apply(it, description) } private fun createStatement(base: Statement) = object : Statement() { @Throws(Throwable::class) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt index e241826..ec0a5b5 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt @@ -9,6 +9,7 @@ import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement import org.koin.android.ext.koin.androidContext +import org.koin.core.context.GlobalContext import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.mp.KoinPlatformTools @@ -22,7 +23,7 @@ import org.koin.test.KoinTest * * Note: Do not use if you want your test's to share Koin, and in such case do not stop your Koin. */ -open class ReloadKoinModulesIfNecessaryTestRule : TestRule, KoinTest { +class ReloadKoinModulesIfNecessaryTestRule : TestRule, KoinTest { override fun apply(base: Statement, description: Description): Statement = ReinitKoinStatement(base) @@ -38,6 +39,7 @@ open class ReloadKoinModulesIfNecessaryTestRule : TestRule, KoinTest { private fun reinitKoinIfNeeded() { if (KoinPlatformTools.defaultContext().getOrNull() != null) return + if (GlobalContext.getOrNull() != null) return val application = ApplicationProvider.getApplicationContext() val baseUrl = BaseUrl(BuildConfig.BASE_URL) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/MainDispatcherTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/MainDispatcherTestRule.kt index 6104117..817617a 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/MainDispatcherTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/MainDispatcherTestRule.kt @@ -12,3 +12,7 @@ interface MainDispatcherTestRule : TestRule { fun advanceTimeBy(delayInMillis: Long) } + +@Suppress("TestFunctionName") +fun MainDispatcherTestRule(): MainDispatcherTestRule = + SpecificTestConfigurationsFactory.createMainDispatcherTestRule() diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt similarity index 59% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt index 135dce1..80c5795 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt @@ -3,14 +3,35 @@ package org.fnives.test.showcase.testutils.idling import androidx.annotation.CheckResult import androidx.test.espresso.IdlingResource import okhttp3.OkHttpClient +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement import org.koin.core.qualifier.StringQualifier import org.koin.test.KoinTest import org.koin.test.get -object NetworkSynchronization : KoinTest { +class NetworkSynchronizationTestRule : TestRule, KoinTest { + + private var disposable: Disposable? = null + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + disposable = registerNetworkingSynchronization() + try { + base.evaluate() + } finally { + dispose() + } + } + + } + } + + fun dispose() = disposable?.dispose() @CheckResult - fun registerNetworkingSynchronization(): Disposable { + private fun registerNetworkingSynchronization(): Disposable { val idlingResources = OkHttpClientTypes.values() .map { it to getOkHttpClient(it) } .associateBy { it.second.dispatcher } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt index febf535..ad90612 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt @@ -2,6 +2,8 @@ package org.fnives.test.showcase.testutils.statesetup import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.intent.Intents +import androidx.test.runner.intent.IntentStubberRegistry import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule @@ -15,7 +17,8 @@ object SetupAuthenticationState : KoinTest { fun setupLogin( mainDispatcherTestRule: MainDispatcherTestRule, - mockServerScenarioSetup: MockServerScenarioSetup + mockServerScenarioSetup: MockServerScenarioSetup, + resetIntents: Boolean = true ) { mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b")) val activityScenario = ActivityScenario.launch(AuthActivity::class.java) @@ -30,19 +33,27 @@ object SetupAuthenticationState : KoinTest { mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed() activityScenario.close() + resetIntentsIfNeeded(resetIntents) } fun setupLogout( - mainDispatcherTestRule: MainDispatcherTestRule + mainDispatcherTestRule: MainDispatcherTestRule, + resetIntents: Boolean = true ) { val activityScenario = ActivityScenario.launch(MainActivity::class.java) activityScenario.moveToState(Lifecycle.State.RESUMED) - val homeRobot = HomeRobot() - homeRobot - .clickSignOut() + HomeRobot().clickSignOut() mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed() activityScenario.close() + resetIntentsIfNeeded(resetIntents) + } + + private fun resetIntentsIfNeeded(resetIntents: Boolean) { + if (resetIntents && IntentStubberRegistry.isLoaded()) { + Intents.release() + Intents.init() + } } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt index c13ef6b..c0a74fc 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt @@ -19,10 +19,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import org.fnives.test.showcase.R import org.fnives.test.showcase.model.content.Content import org.fnives.test.showcase.model.content.FavouriteContent -import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup -import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule import org.fnives.test.showcase.testutils.robot.Robot -import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState import org.fnives.test.showcase.testutils.viewactions.PullToRefresh import org.fnives.test.showcase.testutils.viewactions.WithDrawable import org.fnives.test.showcase.testutils.viewactions.notIntended @@ -33,8 +30,6 @@ class HomeRobot : Robot { override fun init() { Intents.init() - Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName)) - .respondWith(Instrumentation.ActivityResult(0, null)) } override fun release() { @@ -50,6 +45,9 @@ class HomeRobot : Robot { } fun clickSignOut() = apply { + Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName)) + .respondWith(Instrumentation.ActivityResult(0, null)) + Espresso.onView(withId(R.id.logout_cta)).perform(click()) } @@ -103,11 +101,4 @@ class HomeRobot : Robot { Espresso.onView(withId(R.id.error_message)) .check(matches(allOf(isDisplayed(), withText(R.string.something_went_wrong)))) } - - fun setupLogin( - mainDispatcherTestRule: MainDispatcherTestRule, - mockServerScenarioSetup: MockServerScenarioSetup - ) { - SetupAuthenticationState.setupLogin(mainDispatcherTestRule, mockServerScenarioSetup) - } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt index 269d9c5..d8273df 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt @@ -6,17 +6,17 @@ import org.fnives.test.showcase.model.content.FavouriteContent import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario -import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule -import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory -import org.fnives.test.showcase.testutils.idling.Disposable -import org.fnives.test.showcase.testutils.idling.NetworkSynchronization +import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule +import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule import org.fnives.test.showcase.testutils.idling.loopMainThreadFor import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources import org.fnives.test.showcase.testutils.robot.RobotTestRule +import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.koin.test.KoinTest @@ -26,37 +26,26 @@ class MainActivityTest : KoinTest { private lateinit var activityScenario: ActivityScenario - @Rule - @JvmField - val robotRule = RobotTestRule(HomeRobot()) - private val homeRobot get() = robotRule.robot + private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule() + private val mockServerScenarioSetup + get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup + private val mainDispatcherTestRule = MainDispatcherTestRule() + private val robot = HomeRobot() @Rule @JvmField - val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule() - - val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup - - @Rule - @JvmField - val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule() - - private lateinit var disposable: Disposable + val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) + .around(mainDispatcherTestRule) + .around(RobotTestRule(robot)) @Before - fun setUp() { - - disposable = NetworkSynchronization.registerNetworkingSynchronization() - homeRobot.setupLogin( - mainDispatcherTestRule, - mockServerScenarioSetup - ) + fun setup() { + setupLogin(mainDispatcherTestRule, mockServerScenarioSetup) } @After fun tearDown() { activityScenario.close() - disposable.dispose() } /** GIVEN initialized MainActivity WHEN signout is clicked THEN user is signed out */ @@ -66,10 +55,10 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.clickSignOut() + robot.clickSignOut() mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed() - homeRobot.assertNavigatedToAuth() + robot.assertNavigatedToAuth() } /** GIVEN success response WHEN data is returned THEN it is shown on the ui */ @@ -80,9 +69,9 @@ class MainActivityTest : KoinTest { mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() ContentData.contentSuccess.forEachIndexed { index, content -> - homeRobot.assertContainsItem(index, FavouriteContent(content, false)) + robot.assertContainsItem(index, FavouriteContent(content, false)) } - homeRobot.assertDidNotNavigateToAuth() + robot.assertDidNotNavigateToAuth() } /** GIVEN success response WHEN item is clicked THEN ui is updated */ @@ -92,11 +81,11 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.clickOnContentItem(0, ContentData.contentSuccess.first()) + robot.clickOnContentItem(0, ContentData.contentSuccess.first()) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true) - homeRobot.assertContainsItem(0, expectedItem) + robot.assertContainsItem(0, expectedItem) .assertDidNotNavigateToAuth() } @@ -107,7 +96,7 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.clickOnContentItem(0, ContentData.contentSuccess.first()) + robot.clickOnContentItem(0, ContentData.contentSuccess.first()) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true) @@ -116,7 +105,7 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.assertContainsItem(0, expectedItem) + robot.assertContainsItem(0, expectedItem) .assertDidNotNavigateToAuth() } @@ -127,13 +116,13 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.clickOnContentItem(0, ContentData.contentSuccess.first()) + robot.clickOnContentItem(0, ContentData.contentSuccess.first()) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.clickOnContentItem(0, ContentData.contentSuccess.first()) + robot.clickOnContentItem(0, ContentData.contentSuccess.first()) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), false) - homeRobot.assertContainsItem(0, expectedItem) + robot.assertContainsItem(0, expectedItem) .assertDidNotNavigateToAuth() } @@ -144,7 +133,7 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.assertContainsNoItems() + robot.assertContainsNoItems() .assertContainsError() .assertDidNotNavigateToAuth() } @@ -159,14 +148,14 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.swipeRefresh() + robot.swipeRefresh() mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() loopMainThreadFor(2000L) ContentData.contentSuccess.forEachIndexed { index, content -> - homeRobot.assertContainsItem(index, FavouriteContent(content, false)) + robot.assertContainsItem(index, FavouriteContent(content, false)) } - homeRobot.assertDidNotNavigateToAuth() + robot.assertDidNotNavigateToAuth() } /** GIVEN success then error WHEN retried THEN error is shown */ @@ -179,13 +168,13 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.swipeRefresh() + robot.swipeRefresh() mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() loopMainThreadUntilIdleWithIdlingResources() mainDispatcherTestRule.advanceTimeBy(1000L) loopMainThreadFor(1000) - homeRobot + robot .assertContainsError() .assertContainsNoItems() .assertDidNotNavigateToAuth() @@ -204,9 +193,9 @@ class MainActivityTest : KoinTest { mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() ContentData.contentSuccess.forEachIndexed { index, content -> - homeRobot.assertContainsItem(index, FavouriteContent(content, false)) + robot.assertContainsItem(index, FavouriteContent(content, false)) } - homeRobot.assertDidNotNavigateToAuth() + robot.assertDidNotNavigateToAuth() } /** GIVEN unauthenticated then error WHEN loaded THEN navigated to auth */ @@ -218,6 +207,6 @@ class MainActivityTest : KoinTest { activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - homeRobot.assertNavigatedToAuth() + robot.assertNavigatedToAuth() } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt index 26656c6..f167a58 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt @@ -4,16 +4,14 @@ import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario -import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule -import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory -import org.fnives.test.showcase.testutils.idling.Disposable -import org.fnives.test.showcase.testutils.idling.NetworkSynchronization +import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule +import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule import org.fnives.test.showcase.testutils.robot.RobotTestRule import org.fnives.test.showcase.ui.auth.AuthActivity import org.junit.After -import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.koin.test.KoinTest @@ -23,31 +21,20 @@ class AuthActivityTest : KoinTest { private lateinit var activityScenario: ActivityScenario - @Rule - @JvmField - val robotRule = RobotTestRule(LoginRobot()) - private val loginRobot get() = robotRule.robot + private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule() + private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup + private val mainDispatcherTestRule = MainDispatcherTestRule() + private val robot = LoginRobot() @Rule @JvmField - val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule() - val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup - - @Rule - @JvmField - val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule() - - private lateinit var disposable: Disposable - - @Before - fun setUp() { - disposable = NetworkSynchronization.registerNetworkingSynchronization() - } + val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) + .around(mainDispatcherTestRule) + .around(RobotTestRule(robot)) @After fun tearDown() { activityScenario.close() - disposable.dispose() } /** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */ @@ -57,7 +44,7 @@ class AuthActivityTest : KoinTest { AuthScenario.Success(password = "alma", username = "banan") ) activityScenario = ActivityScenario.launch(AuthActivity::class.java) - loginRobot + robot .setPassword("alma") .setUsername("banan") .assertPassword("alma") @@ -66,21 +53,21 @@ class AuthActivityTest : KoinTest { .assertLoadingBeforeRequests() mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed() - loginRobot.assertNavigatedToHome() + robot.assertNavigatedToHome() } /** GIVEN empty password and username WHEN signIn THEN error password is shown */ @Test fun emptyPasswordShowsProperErrorMessage() { activityScenario = ActivityScenario.launch(AuthActivity::class.java) - loginRobot + robot .setUsername("banan") .assertUsername("banan") .clickOnLogin() .assertLoadingBeforeRequests() mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - loginRobot.assertErrorIsShown(R.string.password_is_invalid) + robot.assertErrorIsShown(R.string.password_is_invalid) .assertNotNavigatedToHome() .assertNotLoading() } @@ -89,14 +76,14 @@ class AuthActivityTest : KoinTest { @Test fun emptyUserNameShowsProperErrorMessage() { activityScenario = ActivityScenario.launch(AuthActivity::class.java) - loginRobot + robot .setPassword("banan") .assertPassword("banan") .clickOnLogin() .assertLoadingBeforeRequests() mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - loginRobot.assertErrorIsShown(R.string.username_is_invalid) + robot.assertErrorIsShown(R.string.username_is_invalid) .assertNotNavigatedToHome() .assertNotLoading() } @@ -108,7 +95,7 @@ class AuthActivityTest : KoinTest { AuthScenario.InvalidCredentials(username = "alma", password = "banan") ) activityScenario = ActivityScenario.launch(AuthActivity::class.java) - loginRobot + robot .setUsername("alma") .setPassword("banan") .assertUsername("alma") @@ -117,7 +104,7 @@ class AuthActivityTest : KoinTest { .assertLoadingBeforeRequests() mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - loginRobot.assertErrorIsShown(R.string.credentials_invalid) + robot.assertErrorIsShown(R.string.credentials_invalid) .assertNotNavigatedToHome() .assertNotLoading() } @@ -129,7 +116,7 @@ class AuthActivityTest : KoinTest { AuthScenario.GenericError(username = "alma", password = "banan") ) activityScenario = ActivityScenario.launch(AuthActivity::class.java) - loginRobot + robot .setUsername("alma") .setPassword("banan") .assertUsername("alma") @@ -138,7 +125,7 @@ class AuthActivityTest : KoinTest { .assertLoadingBeforeRequests() mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - loginRobot.assertErrorIsShown(R.string.something_went_wrong) + robot.assertErrorIsShown(R.string.something_went_wrong) .assertNotNavigatedToHome() .assertNotLoading() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt index 71fabde..2e9fc7b 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt @@ -3,15 +3,15 @@ package org.fnives.test.showcase.ui.splash import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule -import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory -import org.fnives.test.showcase.testutils.idling.Disposable -import org.fnives.test.showcase.testutils.idling.NetworkSynchronization +import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule +import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule import org.fnives.test.showcase.testutils.robot.RobotTestRule +import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin +import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogout import org.junit.After -import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.koin.test.KoinTest @@ -21,44 +21,33 @@ class SplashActivityTest : KoinTest { private lateinit var activityScenario: ActivityScenario - private val splashRobot: SplashRobot get() = robotTestRule.robot + private val mainDispatcherTestRule = MainDispatcherTestRule() + private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule() + + private val robot = SplashRobot() @Rule @JvmField - val robotTestRule = RobotTestRule(SplashRobot()) - - @Rule - @JvmField - val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule() - - @Rule - @JvmField - val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule() - - lateinit var disposable: Disposable - - @Before - fun setUp() { - disposable = NetworkSynchronization.registerNetworkingSynchronization() - } + val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule) + .around(mainDispatcherTestRule) + .around(RobotTestRule(robot)) @After fun tearDown() { activityScenario.close() - disposable.dispose() } /** GIVEN loggedInState WHEN opened after some time THEN MainActivity is started */ @Test fun loggedInStateNavigatesToHome() { - splashRobot.setupLoggedInState(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup) + setupLogin(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup) activityScenario = ActivityScenario.launch(SplashActivity::class.java) activityScenario.moveToState(Lifecycle.State.RESUMED) mainDispatcherTestRule.advanceTimeBy(501) - splashRobot.assertHomeIsStarted() + robot.assertHomeIsStarted() .assertAuthIsNotStarted() workaroundForActivityScenarioCLoseLockingUp() @@ -67,13 +56,13 @@ class SplashActivityTest : KoinTest { /** GIVEN loggedOffState WHEN opened after some time THEN AuthActivity is started */ @Test fun loggedOutStatesNavigatesToAuthentication() { - splashRobot.setupLoggedOutState(mainDispatcherTestRule) + setupLogout(mainDispatcherTestRule) activityScenario = ActivityScenario.launch(SplashActivity::class.java) activityScenario.moveToState(Lifecycle.State.RESUMED) mainDispatcherTestRule.advanceTimeBy(501) - splashRobot.assertAuthIsStarted() + robot.assertAuthIsStarted() .assertHomeIsNotStarted() workaroundForActivityScenarioCLoseLockingUp() @@ -82,27 +71,27 @@ class SplashActivityTest : KoinTest { /** GIVEN loggedOffState and not enough time WHEN opened THEN no activity is started */ @Test fun loggedOutStatesNotEnoughTime() { - splashRobot.setupLoggedOutState(mainDispatcherTestRule) + setupLogout(mainDispatcherTestRule) activityScenario = ActivityScenario.launch(SplashActivity::class.java) activityScenario.moveToState(Lifecycle.State.RESUMED) mainDispatcherTestRule.advanceTimeBy(10) - splashRobot.assertAuthIsNotStarted() + robot.assertAuthIsNotStarted() .assertHomeIsNotStarted() } /** GIVEN loggedInState and not enough time WHEN opened THEN no activity is started */ @Test fun loggedInStatesNotEnoughTime() { - splashRobot.setupLoggedInState(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup) + setupLogin(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup) activityScenario = ActivityScenario.launch(SplashActivity::class.java) activityScenario.moveToState(Lifecycle.State.RESUMED) mainDispatcherTestRule.advanceTimeBy(10) - splashRobot.assertHomeIsNotStarted() + robot.assertHomeIsNotStarted() .assertAuthIsNotStarted() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt index 5ed4439..095fea3 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt @@ -1,12 +1,8 @@ package org.fnives.test.showcase.ui.splash -import android.app.Instrumentation import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers -import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup -import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule import org.fnives.test.showcase.testutils.robot.Robot -import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState import org.fnives.test.showcase.testutils.viewactions.notIntended import org.fnives.test.showcase.ui.auth.AuthActivity import org.fnives.test.showcase.ui.home.MainActivity @@ -15,33 +11,12 @@ class SplashRobot : Robot { override fun init() { Intents.init() - Intents.intending(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName)) - .respondWith(Instrumentation.ActivityResult(0, null)) - Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName)) - .respondWith(Instrumentation.ActivityResult(0, null)) } override fun release() { Intents.release() } - fun setupLoggedInState( - mainDispatcherTestRule: MainDispatcherTestRule, - mockServerScenarioSetup: MockServerScenarioSetup - ) { - SetupAuthenticationState.setupLogin(mainDispatcherTestRule, mockServerScenarioSetup) - release() - init() - } - - fun setupLoggedOutState( - mainDispatcherTestRule: MainDispatcherTestRule - ) { - SetupAuthenticationState.setupLogout(mainDispatcherTestRule) - release() - init() - } - fun assertHomeIsStarted() = apply { Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName)) } From b9644512d51f4f4dc5a065d9a0d0f5611005ead6 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 00:43:08 +0200 Subject: [PATCH 07/17] Issue#13 Remove reference to SpecificConfiguration --- ...tRule.kt => AndroidTestSnackbarVerificationHelper.kt} | 2 +- .../configuration/SpecificTestConfigurationsFactory.kt | 4 ++-- ...tRule.kt => RobolectricSnackbarVerificationHelper.kt} | 2 +- .../configuration/SpecificTestConfigurationsFactory.kt | 4 ++-- ...ficationTestRule.kt => SnackbarVerificationHelper.kt} | 6 +++++- .../testutils/configuration/TestConfigurationsFactory.kt | 2 +- .../java/org/fnives/test/showcase/ui/login/LoginRobot.kt | 9 +++++---- 7 files changed, 17 insertions(+), 12 deletions(-) rename app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/{AndroidTestSnackbarVerificationTestRule.kt => AndroidTestSnackbarVerificationHelper.kt} (95%) rename app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/{RobolectricSnackbarVerificationTestRule.kt => RobolectricSnackbarVerificationHelper.kt} (85%) rename app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/{SnackbarVerificationTestRule.kt => SnackbarVerificationHelper.kt} (50%) diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestSnackbarVerificationTestRule.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestSnackbarVerificationHelper.kt similarity index 95% rename from app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestSnackbarVerificationTestRule.kt rename to app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestSnackbarVerificationHelper.kt index 9a12237..411a634 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestSnackbarVerificationTestRule.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestSnackbarVerificationHelper.kt @@ -15,7 +15,7 @@ import org.hamcrest.Matchers import org.junit.runner.Description import org.junit.runners.model.Statement -object AndroidTestSnackbarVerificationTestRule : SnackbarVerificationTestRule { +object AndroidTestSnackbarVerificationHelper : SnackbarVerificationHelper { override fun apply(base: Statement, description: Description): Statement = base diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt index 6a3da36..ad369fb 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt @@ -7,8 +7,8 @@ object SpecificTestConfigurationsFactory : TestConfigurationsFactory { override fun createLoginRobotConfiguration(): LoginRobotConfiguration = AndroidTestLoginRobotConfiguration - override fun createSnackbarVerification(): SnackbarVerificationTestRule = - AndroidTestSnackbarVerificationTestRule + override fun createSnackbarVerification(): SnackbarVerificationHelper = + AndroidTestSnackbarVerificationHelper override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory = AndroidMigrationTestRuleFactory diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricSnackbarVerificationTestRule.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricSnackbarVerificationHelper.kt similarity index 85% rename from app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricSnackbarVerificationTestRule.kt rename to app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricSnackbarVerificationHelper.kt index 565a518..edde912 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricSnackbarVerificationTestRule.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricSnackbarVerificationHelper.kt @@ -6,7 +6,7 @@ import org.fnives.test.showcase.testutils.shadow.ShadowSnackbarResetTestRule import org.junit.Assert import org.junit.rules.TestRule -object RobolectricSnackbarVerificationTestRule : SnackbarVerificationTestRule, TestRule by ShadowSnackbarResetTestRule() { +object RobolectricSnackbarVerificationHelper : SnackbarVerificationHelper, TestRule by ShadowSnackbarResetTestRule() { override fun assertIsShownWithText(@StringRes stringResID: Int) { val latestSnackbar = ShadowSnackbar.latestSnackbar ?: throw IllegalStateException("Snackbar not found") diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt index ff7e7af..8df6d58 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt @@ -7,8 +7,8 @@ object SpecificTestConfigurationsFactory : TestConfigurationsFactory { override fun createLoginRobotConfiguration(): LoginRobotConfiguration = RobolectricLoginRobotConfiguration - override fun createSnackbarVerification(): SnackbarVerificationTestRule = - RobolectricSnackbarVerificationTestRule + override fun createSnackbarVerification(): SnackbarVerificationHelper = + RobolectricSnackbarVerificationHelper override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory = RobolectricMigrationTestHelperFactory diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt similarity index 50% rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationTestRule.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt index fa89428..2520813 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt @@ -3,9 +3,13 @@ package org.fnives.test.showcase.testutils.configuration import androidx.annotation.StringRes import org.junit.rules.TestRule -interface SnackbarVerificationTestRule : TestRule { +interface SnackbarVerificationHelper : TestRule { fun assertIsShownWithText(@StringRes stringResID: Int) fun assertIsNotShown() } + +@Suppress("TestFunctionName") +fun SnackbarVerificationTestRule(): SnackbarVerificationHelper= + SpecificTestConfigurationsFactory.createSnackbarVerification() \ No newline at end of file diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt index 3ac0428..84614bb 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt @@ -12,7 +12,7 @@ interface TestConfigurationsFactory { fun createLoginRobotConfiguration(): LoginRobotConfiguration - fun createSnackbarVerification(): SnackbarVerificationTestRule + fun createSnackbarVerification(): SnackbarVerificationHelper fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt index 4e8aba6..c14b7de 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt @@ -16,6 +16,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import org.fnives.test.showcase.R import org.fnives.test.showcase.testutils.configuration.LoginRobotConfiguration +import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationTestRule import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory import org.fnives.test.showcase.testutils.configuration.TestConfigurationsFactory @@ -26,13 +27,13 @@ import org.hamcrest.core.IsNot.not class LoginRobot( private val loginRobotConfiguration: LoginRobotConfiguration, - private val snackbarVerificationTestRule: SnackbarVerificationTestRule + private val snackbarVerificationHelper: SnackbarVerificationHelper ) : Robot { constructor(testConfigurationsFactory: TestConfigurationsFactory = SpecificTestConfigurationsFactory) : this( loginRobotConfiguration = testConfigurationsFactory.createLoginRobotConfiguration(), - snackbarVerificationTestRule = testConfigurationsFactory.createSnackbarVerification() + snackbarVerificationHelper = SnackbarVerificationTestRule() ) override fun init() { @@ -75,7 +76,7 @@ class LoginRobot( } fun assertErrorIsShown(@StringRes stringResID: Int) = apply { - snackbarVerificationTestRule.assertIsShownWithText(stringResID) + snackbarVerificationHelper.assertIsShownWithText(stringResID) } fun assertLoadingBeforeRequests() = apply { @@ -91,7 +92,7 @@ class LoginRobot( } fun assertErrorIsNotShown() = apply { - snackbarVerificationTestRule.assertIsNotShown() + snackbarVerificationHelper.assertIsNotShown() } fun assertNavigatedToHome() = apply { From 5d89e6235639b2ed3bab83e266dd1f96b65a96b5 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 01:54:43 +0200 Subject: [PATCH 08/17] Issue#13 Remove unnecessary configurations and use TestDispatcher on both side Additionally added workaround for progressbar testing --- .../AndroidTestLoginRobotConfiguration.kt | 5 -- .../AndroidTestMainDispatcherTestRule.kt | 51 ------------------- .../SpecificTestConfigurationsFactory.kt | 5 -- .../RobolectricLoginRobotConfiguration.kt | 5 -- .../SpecificTestConfigurationsFactory.kt | 5 -- .../testutils/ActivityScenarioExtensions.kt | 22 ++++++++ .../configuration/LoginRobotConfiguration.kt | 6 --- .../configuration/MainDispatcherTestRule.kt | 18 ------- .../TestConfigurationsFactory.kt | 4 -- .../testutils/doBlockinglyOnMainThread.kt | 2 +- .../idling/MainDispatcherTestRule.kt} | 26 ++++++---- .../testutils/idling/awaitIdlingResources.kt | 19 +++---- .../statesetup/SetupAuthenticationState.kt | 12 ++--- .../testutils/viewactions/PullToRefresh.kt | 4 +- .../ReplaceProgressBarDrawableToStatic.kt | 24 +++++++++ .../test/showcase/ui/home/MainActivityTest.kt | 9 ++-- .../showcase/ui/login/AuthActivityTest.kt | 7 +-- .../test/showcase/ui/login/LoginRobot.kt | 32 ++++++------ .../showcase/ui/splash/SplashActivityTest.kt | 26 ++-------- 19 files changed, 108 insertions(+), 174 deletions(-) delete mode 100644 app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestLoginRobotConfiguration.kt delete mode 100644 app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestMainDispatcherTestRule.kt delete mode 100644 app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricLoginRobotConfiguration.kt create mode 100644 app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt delete mode 100644 app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/LoginRobotConfiguration.kt delete mode 100644 app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/MainDispatcherTestRule.kt rename app/src/{robolectricTest/java/org/fnives/test/showcase/testutils/configuration/TestCoroutineMainDispatcherTestRule.kt => sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt} (62%) create mode 100644 app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestLoginRobotConfiguration.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestLoginRobotConfiguration.kt deleted file mode 100644 index edccc1f..0000000 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestLoginRobotConfiguration.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -object AndroidTestLoginRobotConfiguration : LoginRobotConfiguration { - override val assertLoadingBeforeRequest: Boolean get() = false -} diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestMainDispatcherTestRule.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestMainDispatcherTestRule.kt deleted file mode 100644 index 2a85cbd..0000000 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestMainDispatcherTestRule.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -import androidx.test.espresso.Espresso -import androidx.test.espresso.NoActivityResumedException -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.matcher.ViewMatchers -import kotlinx.coroutines.Dispatchers -import org.fnives.test.showcase.storage.database.DatabaseInitialization -import org.fnives.test.showcase.testutils.idling.loopMainThreadFor -import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources -import org.junit.runner.Description -import org.junit.runners.model.Statement - -class AndroidTestMainDispatcherTestRule : MainDispatcherTestRule { - - override fun apply(base: Statement, description: Description): Statement = - object : Statement() { - @Throws(Throwable::class) - override fun evaluate() { - DatabaseInitialization.dispatcher = Dispatchers.Main - base.evaluate() - } - } - - override fun advanceUntilIdleWithIdlingResources() { - loopMainThreadUntilIdleWithIdlingResources() - } - - override fun advanceUntilIdleOrActivityIsDestroyed() { - try { - advanceUntilIdleWithIdlingResources() - Espresso.onView(ViewMatchers.isRoot()).check(ViewAssertions.doesNotExist()) - } catch (noActivityResumedException: NoActivityResumedException) { - // expected to happen - } catch (runtimeException: RuntimeException) { - if (runtimeException.message?.contains("No activities found") == true) { - // expected to happen - } else { - throw runtimeException - } - } - } - - override fun advanceUntilIdle() { - loopMainThreadUntilIdleWithIdlingResources() - } - - override fun advanceTimeBy(delayInMillis: Long) { - loopMainThreadFor(delayInMillis) - } -} diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt index ad369fb..f35cb9d 100644 --- a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt +++ b/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt @@ -1,11 +1,6 @@ package org.fnives.test.showcase.testutils.configuration object SpecificTestConfigurationsFactory : TestConfigurationsFactory { - override fun createMainDispatcherTestRule(): MainDispatcherTestRule = - AndroidTestMainDispatcherTestRule() - - override fun createLoginRobotConfiguration(): LoginRobotConfiguration = - AndroidTestLoginRobotConfiguration override fun createSnackbarVerification(): SnackbarVerificationHelper = AndroidTestSnackbarVerificationHelper diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricLoginRobotConfiguration.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricLoginRobotConfiguration.kt deleted file mode 100644 index dbe6927..0000000 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/RobolectricLoginRobotConfiguration.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -object RobolectricLoginRobotConfiguration : LoginRobotConfiguration { - override val assertLoadingBeforeRequest: Boolean = true -} diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt index 8df6d58..e7ef0bb 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/SpecificTestConfigurationsFactory.kt @@ -1,11 +1,6 @@ package org.fnives.test.showcase.testutils.configuration object SpecificTestConfigurationsFactory : TestConfigurationsFactory { - override fun createMainDispatcherTestRule(): MainDispatcherTestRule = - TestCoroutineMainDispatcherTestRule() - - override fun createLoginRobotConfiguration(): LoginRobotConfiguration = - RobolectricLoginRobotConfiguration override fun createSnackbarVerification(): SnackbarVerificationHelper = RobolectricSnackbarVerificationHelper diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt new file mode 100644 index 0000000..345ea86 --- /dev/null +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt @@ -0,0 +1,22 @@ +package org.fnives.test.showcase.testutils + +import android.app.Activity +import androidx.test.core.app.ActivityScenario + +fun ActivityScenario.safeClose() { + workaroundForActivityScenarioCLoseLockingUp() + close() +} + +/** + * This should not be needed, we shouldn't use sleep ever. + * However, it seems to be and issue described here: https://github.com/android/android-test/issues/676 + * + * If an activity is finished in code, the ActivityScenario.close() can hang 30 to 45 seconds. + * This sleeps let's the Activity finish it state change and unlocks the ActivityScenario. + * + * As soon as that issue is closed, this should be removed as well. + */ +private fun workaroundForActivityScenarioCLoseLockingUp() { + Thread.sleep(1000L) +} \ No newline at end of file diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/LoginRobotConfiguration.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/LoginRobotConfiguration.kt deleted file mode 100644 index 1cc74df..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/LoginRobotConfiguration.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -interface LoginRobotConfiguration { - - val assertLoadingBeforeRequest: Boolean -} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/MainDispatcherTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/MainDispatcherTestRule.kt deleted file mode 100644 index 817617a..0000000 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/MainDispatcherTestRule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.fnives.test.showcase.testutils.configuration - -import org.junit.rules.TestRule - -interface MainDispatcherTestRule : TestRule { - - fun advanceUntilIdleWithIdlingResources() - - fun advanceUntilIdleOrActivityIsDestroyed() - - fun advanceUntilIdle() - - fun advanceTimeBy(delayInMillis: Long) -} - -@Suppress("TestFunctionName") -fun MainDispatcherTestRule(): MainDispatcherTestRule = - SpecificTestConfigurationsFactory.createMainDispatcherTestRule() diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt index 84614bb..3dc5eb3 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/TestConfigurationsFactory.kt @@ -8,10 +8,6 @@ package org.fnives.test.showcase.testutils.configuration */ interface TestConfigurationsFactory { - fun createMainDispatcherTestRule(): MainDispatcherTestRule - - fun createLoginRobotConfiguration(): LoginRobotConfiguration - fun createSnackbarVerification(): SnackbarVerificationHelper fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/doBlockinglyOnMainThread.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/doBlockinglyOnMainThread.kt index f9aa3bd..f8a8e91 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/doBlockinglyOnMainThread.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/doBlockinglyOnMainThread.kt @@ -5,7 +5,7 @@ import android.os.Looper import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.runBlocking -fun doBlockinglyOnMainThread(action: () -> Unit) { +fun runOnUIAwaitOnCurrent(action: () -> Unit) { if (Looper.myLooper() === Looper.getMainLooper()) { action() } else { diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/TestCoroutineMainDispatcherTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt similarity index 62% rename from app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/TestCoroutineMainDispatcherTestRule.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt index 5460a89..68dd4c4 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/testutils/configuration/TestCoroutineMainDispatcherTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/MainDispatcherTestRule.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.testutils.configuration +package org.fnives.test.showcase.testutils.idling import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -8,12 +8,13 @@ import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.fnives.test.showcase.storage.database.DatabaseInitialization -import org.fnives.test.showcase.testutils.idling.advanceUntilIdleWithIdlingResources +import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent +import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @OptIn(ExperimentalCoroutinesApi::class) -class TestCoroutineMainDispatcherTestRule : MainDispatcherTestRule { +class MainDispatcherTestRule : TestRule { private lateinit var testDispatcher: TestDispatcher @@ -33,19 +34,24 @@ class TestCoroutineMainDispatcherTestRule : MainDispatcherTestRule { } } - override fun advanceUntilIdleWithIdlingResources() { + fun advanceUntilIdleWithIdlingResources() = runOnUIAwaitOnCurrent { testDispatcher.advanceUntilIdleWithIdlingResources() } - override fun advanceUntilIdleOrActivityIsDestroyed() { - advanceUntilIdleWithIdlingResources() - } - - override fun advanceUntilIdle() { + fun advanceUntilIdle() = runOnUIAwaitOnCurrent { testDispatcher.scheduler.advanceUntilIdle() } - override fun advanceTimeBy(delayInMillis: Long) { + fun advanceTimeBy(delayInMillis: Long) = runOnUIAwaitOnCurrent { testDispatcher.scheduler.advanceTimeBy(delayInMillis) } + + private fun TestDispatcher.advanceUntilIdleWithIdlingResources() { + scheduler.advanceUntilIdle() // advance until a request is sent + while (anyResourceIdling()) { // check if any request is in progress + awaitIdlingResources() // complete all requests and other idling resources + scheduler.advanceUntilIdle() // run coroutines after request is finished + } + scheduler.advanceUntilIdle() + } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt index 80a7d15..b0a1264 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt @@ -1,11 +1,10 @@ package org.fnives.test.showcase.testutils.idling +import android.os.Looper import androidx.test.espresso.Espresso import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingResource import androidx.test.espresso.matcher.ViewMatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestDispatcher import org.fnives.test.showcase.testutils.viewactions.LoopMainThreadFor import org.fnives.test.showcase.testutils.viewactions.LoopMainThreadUntilIdle import java.util.concurrent.Executors @@ -43,16 +42,6 @@ private fun IdlingResource.awaitUntilIdle() { } } -@OptIn(ExperimentalCoroutinesApi::class) -fun TestDispatcher.advanceUntilIdleWithIdlingResources() { - scheduler.advanceUntilIdle() // advance until a request is sent - while (anyResourceIdling()) { // check if any request is in progress - awaitIdlingResources() // complete all requests and other idling resources - scheduler.advanceUntilIdle() // run coroutines after request is finished - } - scheduler.advanceUntilIdle() -} - fun loopMainThreadUntilIdleWithIdlingResources() { Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadUntilIdle()) // advance until a request is sent while (anyResourceIdling()) { // check if any request is in progress @@ -63,5 +52,9 @@ fun loopMainThreadUntilIdleWithIdlingResources() { } fun loopMainThreadFor(delay: Long) { - Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(delay)) + if (Looper.getMainLooper().isCurrentThread){ + Thread.sleep(200L) + } else { + Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(delay)) + } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt index ad90612..305d61b 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt @@ -6,7 +6,8 @@ import androidx.test.espresso.intent.Intents import androidx.test.runner.intent.IntentStubberRegistry import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario -import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule +import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule +import org.fnives.test.showcase.testutils.safeClose import org.fnives.test.showcase.ui.auth.AuthActivity import org.fnives.test.showcase.ui.home.HomeRobot import org.fnives.test.showcase.ui.home.MainActivity @@ -30,9 +31,9 @@ object SetupAuthenticationState : KoinTest { .setUsername("a") .clickOnLogin() - mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed() + mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - activityScenario.close() + activityScenario.safeClose() resetIntentsIfNeeded(resetIntents) } @@ -43,10 +44,9 @@ object SetupAuthenticationState : KoinTest { val activityScenario = ActivityScenario.launch(MainActivity::class.java) activityScenario.moveToState(Lifecycle.State.RESUMED) HomeRobot().clickSignOut() + mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() - mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed() - - activityScenario.close() + activityScenario.safeClose() resetIntentsIfNeeded(resetIntents) } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/PullToRefresh.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/PullToRefresh.kt index 39f9c85..fa8a14c 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/PullToRefresh.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/PullToRefresh.kt @@ -5,7 +5,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.listener import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction -import org.fnives.test.showcase.testutils.doBlockinglyOnMainThread +import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent import org.hamcrest.BaseMatcher import org.hamcrest.CoreMatchers.isA import org.hamcrest.Description @@ -36,7 +36,7 @@ class PullToRefresh : ViewAction { override fun perform(uiController: UiController, view: View) { val swipeRefreshLayout = view as SwipeRefreshLayout - doBlockinglyOnMainThread { + runOnUIAwaitOnCurrent { swipeRefreshLayout.isRefreshing = true swipeRefreshLayout.listener().onRefresh() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt new file mode 100644 index 0000000..b79192e --- /dev/null +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt @@ -0,0 +1,24 @@ +package org.fnives.test.showcase.testutils.viewactions + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.View +import android.widget.ProgressBar +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import org.hamcrest.Matcher + +class ReplaceProgressBarDrawableToStatic : ViewAction { + override fun getConstraints(): Matcher = + isAssignableFrom(ProgressBar::class.java) + + override fun getDescription(): String = + "replace the ProgressBar drawable" + + override fun perform(uiController: UiController, view: View) { + val progressBar: ProgressBar = view as ProgressBar + progressBar.indeterminateDrawable = ColorDrawable(Color.GREEN) + uiController.loopMainThreadUntilIdle() + } +} \ No newline at end of file diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt index d8273df..ff4fd8e 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt @@ -7,10 +7,11 @@ import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule -import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule +import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule import org.fnives.test.showcase.testutils.idling.loopMainThreadFor import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources import org.fnives.test.showcase.testutils.robot.RobotTestRule +import org.fnives.test.showcase.testutils.safeClose import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin import org.junit.After import org.junit.Before @@ -45,7 +46,7 @@ class MainActivityTest : KoinTest { @After fun tearDown() { - activityScenario.close() + activityScenario.safeClose() } /** GIVEN initialized MainActivity WHEN signout is clicked THEN user is signed out */ @@ -56,7 +57,7 @@ class MainActivityTest : KoinTest { mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() robot.clickSignOut() - mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed() + mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() robot.assertNavigatedToAuth() } @@ -101,7 +102,7 @@ class MainActivityTest : KoinTest { val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true) - activityScenario.close() + activityScenario.safeClose() activityScenario = ActivityScenario.launch(MainActivity::class.java) mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt index f167a58..cfa3e78 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt @@ -5,8 +5,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.R import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule -import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule +import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule import org.fnives.test.showcase.testutils.robot.RobotTestRule +import org.fnives.test.showcase.testutils.safeClose import org.fnives.test.showcase.ui.auth.AuthActivity import org.junit.After import org.junit.Rule @@ -34,7 +35,7 @@ class AuthActivityTest : KoinTest { @After fun tearDown() { - activityScenario.close() + activityScenario.safeClose() } /** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */ @@ -52,7 +53,7 @@ class AuthActivityTest : KoinTest { .clickOnLogin() .assertLoadingBeforeRequests() - mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed() + mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() robot.assertNavigatedToHome() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt index c14b7de..1210057 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt @@ -15,27 +15,18 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import org.fnives.test.showcase.R -import org.fnives.test.showcase.testutils.configuration.LoginRobotConfiguration import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationTestRule -import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory -import org.fnives.test.showcase.testutils.configuration.TestConfigurationsFactory import org.fnives.test.showcase.testutils.robot.Robot +import org.fnives.test.showcase.testutils.viewactions.ReplaceProgressBarDrawableToStatic import org.fnives.test.showcase.testutils.viewactions.notIntended import org.fnives.test.showcase.ui.home.MainActivity import org.hamcrest.core.IsNot.not class LoginRobot( - private val loginRobotConfiguration: LoginRobotConfiguration, - private val snackbarVerificationHelper: SnackbarVerificationHelper + private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationTestRule() ) : Robot { - constructor(testConfigurationsFactory: TestConfigurationsFactory = SpecificTestConfigurationsFactory) : - this( - loginRobotConfiguration = testConfigurationsFactory.createLoginRobotConfiguration(), - snackbarVerificationHelper = SnackbarVerificationTestRule() - ) - override fun init() { Intents.init() setupIntentResults() @@ -50,6 +41,18 @@ class LoginRobot( Intents.release() } + /** + * Needed because Espresso idling waits until mainThread is idle. + * + * However, ProgressBar keeps the main thread active since it's animating. + * + * Another solution is described here: https://proandroiddev.com/progressbar-animations-with-espresso-57f826102187 + * In short they replace the inflater to remove animations, by using custom test runner. + */ + fun replaceProgressBar() = apply { + onView(withId(R.id.loading_indicator)).perform(ReplaceProgressBarDrawableToStatic()) + } + fun setUsername(username: String): LoginRobot = apply { onView(withId(R.id.user_edit_text)) .perform(ViewActions.replaceText(username), ViewActions.closeSoftKeyboard()) @@ -61,6 +64,7 @@ class LoginRobot( } fun clickOnLogin() = apply { + replaceProgressBar() onView(withId(R.id.login_cta)) .perform(ViewActions.click()) } @@ -80,10 +84,8 @@ class LoginRobot( } fun assertLoadingBeforeRequests() = apply { - if (loginRobotConfiguration.assertLoadingBeforeRequest) { - onView(withId(R.id.loading_indicator)) - .check(ViewAssertions.matches(isDisplayed())) - } + onView(withId(R.id.loading_indicator)) + .check(ViewAssertions.matches(isDisplayed())) } fun assertNotLoading() = apply { diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt index 2e9fc7b..91ab302 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt @@ -4,8 +4,9 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule -import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule +import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule import org.fnives.test.showcase.testutils.robot.RobotTestRule +import org.fnives.test.showcase.testutils.safeClose import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogout import org.junit.After @@ -34,7 +35,7 @@ class SplashActivityTest : KoinTest { @After fun tearDown() { - activityScenario.close() + activityScenario.safeClose() } /** GIVEN loggedInState WHEN opened after some time THEN MainActivity is started */ @@ -49,8 +50,6 @@ class SplashActivityTest : KoinTest { robot.assertHomeIsStarted() .assertAuthIsNotStarted() - - workaroundForActivityScenarioCLoseLockingUp() } /** GIVEN loggedOffState WHEN opened after some time THEN AuthActivity is started */ @@ -64,8 +63,6 @@ class SplashActivityTest : KoinTest { robot.assertAuthIsStarted() .assertHomeIsNotStarted() - - workaroundForActivityScenarioCLoseLockingUp() } /** GIVEN loggedOffState and not enough time WHEN opened THEN no activity is started */ @@ -75,7 +72,7 @@ class SplashActivityTest : KoinTest { activityScenario = ActivityScenario.launch(SplashActivity::class.java) activityScenario.moveToState(Lifecycle.State.RESUMED) - mainDispatcherTestRule.advanceTimeBy(10) + mainDispatcherTestRule.advanceTimeBy(500) robot.assertAuthIsNotStarted() .assertHomeIsNotStarted() @@ -89,22 +86,9 @@ class SplashActivityTest : KoinTest { activityScenario = ActivityScenario.launch(SplashActivity::class.java) activityScenario.moveToState(Lifecycle.State.RESUMED) - mainDispatcherTestRule.advanceTimeBy(10) + mainDispatcherTestRule.advanceTimeBy(500) robot.assertHomeIsNotStarted() .assertAuthIsNotStarted() } - - /** - * This should not be needed, we shouldn't use sleep ever. - * However, it seems to be and issue described here: https://github.com/android/android-test/issues/676 - * - * If an activity is finished in code, the ActivityScenario.close() can hang 30 to 45 seconds. - * This sleeps let's the Activity finish it state change and unlocks the ActivityScenario. - * - * As soon as that issue is closed, this should be removed as well. - */ - private fun workaroundForActivityScenarioCLoseLockingUp() { - Thread.sleep(1000L) - } } From f248ab10815c9b0e6cebd29c6186dd279f5d7feb Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 02:10:03 +0200 Subject: [PATCH 09/17] Issue#13 Fix codeAnalysis errors --- .../test/showcase/testutils/ActivityScenarioExtensions.kt | 4 ++-- .../testutils/MockServerScenarioSetupResetingTestRule.kt | 1 - .../testutils/configuration/SnackbarVerificationHelper.kt | 4 ++-- .../testutils/idling/NetworkSynchronizationTestRule.kt | 1 - .../test/showcase/testutils/idling/awaitIdlingResources.kt | 2 +- .../viewactions/ReplaceProgressBarDrawableToStatic.kt | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt index 345ea86..1bf914c 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ActivityScenarioExtensions.kt @@ -3,7 +3,7 @@ package org.fnives.test.showcase.testutils import android.app.Activity import androidx.test.core.app.ActivityScenario -fun ActivityScenario.safeClose() { +fun ActivityScenario.safeClose() { workaroundForActivityScenarioCLoseLockingUp() close() } @@ -19,4 +19,4 @@ fun ActivityScenario.safeClose() { */ private fun workaroundForActivityScenarioCLoseLockingUp() { Thread.sleep(1000L) -} \ No newline at end of file +} diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt index cd1c405..72b48fe 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt @@ -71,7 +71,6 @@ class MockServerScenarioSetupResetingTestRule( .sslSocketFactory(handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager) .build() - private fun after() { mockServerScenarioSetup.stop() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt index 2520813..0adb1d9 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/configuration/SnackbarVerificationHelper.kt @@ -11,5 +11,5 @@ interface SnackbarVerificationHelper : TestRule { } @Suppress("TestFunctionName") -fun SnackbarVerificationTestRule(): SnackbarVerificationHelper= - SpecificTestConfigurationsFactory.createSnackbarVerification() \ No newline at end of file +fun SnackbarVerificationTestRule(): SnackbarVerificationHelper = + SpecificTestConfigurationsFactory.createSnackbarVerification() diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt index 80c5795..dc97866 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt @@ -24,7 +24,6 @@ class NetworkSynchronizationTestRule : TestRule, KoinTest { dispose() } } - } } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt index b0a1264..21996b5 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/awaitIdlingResources.kt @@ -52,7 +52,7 @@ fun loopMainThreadUntilIdleWithIdlingResources() { } fun loopMainThreadFor(delay: Long) { - if (Looper.getMainLooper().isCurrentThread){ + if (Looper.getMainLooper().isCurrentThread) { Thread.sleep(200L) } else { Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(delay)) diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt index b79192e..44a6367 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/viewactions/ReplaceProgressBarDrawableToStatic.kt @@ -21,4 +21,4 @@ class ReplaceProgressBarDrawableToStatic : ViewAction { progressBar.indeterminateDrawable = ColorDrawable(Color.GREEN) uiController.loopMainThreadUntilIdle() } -} \ No newline at end of file +} From c4c2ea7c263fbade8ffe0a7ccbd4f2fb8946b46f Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 03:21:55 +0200 Subject: [PATCH 10/17] Issue#13 Add separate task for robolectric tests --- ...ouriteContentLocalStorageInstrumentedTest.kt} | 2 +- ...teContentLocalStorageImplInstrumentedTest.kt} | 2 +- ...t.kt => MigrationToLatestInstrumentedTest.kt} | 2 +- ...tyTest.kt => MainActivityInstrumentedTest.kt} | 2 +- ...tyTest.kt => AuthActivityInstrumentedTest.kt} | 2 +- ...Test.kt => SplashActivityInstrumentedTest.kt} | 2 +- build.gradle | 15 --------------- codekata/robolectric.instructionset.md | 2 +- gradlescripts/testoptions.gradle | 16 ++++++++++++++++ 9 files changed, 23 insertions(+), 22 deletions(-) rename app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/{CodeKataFavouriteContentLocalStorage.kt => CodeKataFavouriteContentLocalStorageInstrumentedTest.kt} (95%) rename app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/{FavouriteContentLocalStorageImplTest.kt => FavouriteContentLocalStorageImplInstrumentedTest.kt} (98%) rename app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/{MigrationToLatest.kt => MigrationToLatestInstrumentedTest.kt} (98%) rename app/src/sharedTest/java/org/fnives/test/showcase/ui/home/{MainActivityTest.kt => MainActivityInstrumentedTest.kt} (99%) rename app/src/sharedTest/java/org/fnives/test/showcase/ui/login/{AuthActivityTest.kt => AuthActivityInstrumentedTest.kt} (99%) rename app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/{SplashActivityTest.kt => SplashActivityInstrumentedTest.kt} (98%) diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorage.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorageInstrumentedTest.kt similarity index 95% rename from app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorage.kt rename to app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorageInstrumentedTest.kt index f84bf89..9ab12ae 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorage.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/CodeKataFavouriteContentLocalStorageInstrumentedTest.kt @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Disabled @Disabled("CodeKata") @OptIn(ExperimentalCoroutinesApi::class) -class CodeKataFavouriteContentLocalStorage { +class CodeKataFavouriteContentLocalStorageInstrumentedTest { @Before fun setUp() { diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplTest.kt b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplInstrumentedTest.kt similarity index 98% rename from app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplTest.kt rename to app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplInstrumentedTest.kt index 479e5f6..b527b76 100644 --- a/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplTest.kt +++ b/app/src/robolectricTest/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImplInstrumentedTest.kt @@ -26,7 +26,7 @@ import org.koin.test.inject @Suppress("TestFunctionName") @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) -internal class FavouriteContentLocalStorageImplTest : KoinTest { +internal class FavouriteContentLocalStorageImplInstrumentedTest : KoinTest { private val sut by inject() private lateinit var testDispatcher: TestDispatcher diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatestInstrumentedTest.kt similarity index 98% rename from app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatest.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatestInstrumentedTest.kt index 75cc71b..d615a49 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/storage/migration/MigrationToLatestInstrumentedTest.kt @@ -25,7 +25,7 @@ import java.io.IOException * https://developer.android.com/training/data-storage/room/migrating-db-versions */ @RunWith(AndroidJUnit4::class) -class MigrationToLatest { +class MigrationToLatestInstrumentedTest { @get:Rule val helper: SharedMigrationTestRule = createSharedMigrationTestRule( diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt similarity index 99% rename from app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt index ff4fd8e..eb77d0b 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityInstrumentedTest.kt @@ -23,7 +23,7 @@ import org.koin.test.KoinTest @Suppress("TestFunctionName") @RunWith(AndroidJUnit4::class) -class MainActivityTest : KoinTest { +class MainActivityInstrumentedTest : KoinTest { private lateinit var activityScenario: ActivityScenario diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt similarity index 99% rename from app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt index cfa3e78..af9f454 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityInstrumentedTest.kt @@ -18,7 +18,7 @@ import org.koin.test.KoinTest @Suppress("TestFunctionName") @RunWith(AndroidJUnit4::class) -class AuthActivityTest : KoinTest { +class AuthActivityInstrumentedTest : KoinTest { private lateinit var activityScenario: ActivityScenario diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt similarity index 98% rename from app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt rename to app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt index 91ab302..50c5359 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityInstrumentedTest.kt @@ -18,7 +18,7 @@ import org.koin.test.KoinTest @Suppress("TestFunctionName") @RunWith(AndroidJUnit4::class) -class SplashActivityTest : KoinTest { +class SplashActivityInstrumentedTest : KoinTest { private lateinit var activityScenario: ActivityScenario diff --git a/build.gradle b/build.gradle index d96f97a..9c84311 100644 --- a/build.gradle +++ b/build.gradle @@ -29,21 +29,6 @@ task clean(type: Delete) { delete rootProject.buildDir } -task unitTests(dependsOn: ["app:testDebugUnitTest", "core:test", "network:test"]){ - group = 'Tests' - description = 'Run all unit tests' -} - -task robolectricTests(dependsOn: ["app:testDebugUnitTest"]){ - group = 'Tests' - description = 'Run all robolectric tests' -} - -task androidTests(dependsOn: ["app:connectedDebugAndroidTest"]){ - group = 'Tests' - description = 'Run all Android tests' -} - apply from: 'gradlescripts/versions.gradle' apply from: 'gradlescripts/detekt.config.gradle' apply from: 'gradlescripts/ktlint.gradle' diff --git a/codekata/robolectric.instructionset.md b/codekata/robolectric.instructionset.md index 3c3aab5..1889fad 100644 --- a/codekata/robolectric.instructionset.md +++ b/codekata/robolectric.instructionset.md @@ -23,7 +23,7 @@ What it does is: So let's start with the setup. -Our test class is `org.fnives.test.showcase.storage.favourite.CodeKataFavouriteContentLocalStorage` +Our test class is `org.fnives.test.showcase.storage.favourite.CodeKataFavouriteContentLocalStorageInstrumentedTest` Question: Why don't we test the DAO and Storage separately using mocking? Answer: The same logic applies how we didn't test the RetrofitServices just the RemoteSources. The Service just like the DAO is an implementation detail, our code only accesses them through the RemoteSource / LocalStorage abstraction. With this in mind now we only want to test that we interact with the database properly, we don't really care how many DAOs are used. diff --git a/gradlescripts/testoptions.gradle b/gradlescripts/testoptions.gradle index 7193f48..9c0e171 100644 --- a/gradlescripts/testoptions.gradle +++ b/gradlescripts/testoptions.gradle @@ -62,4 +62,20 @@ subprojects { module -> } } } +} + +task jvmTests(dependsOn: ["app:testDebugUnitTest", "core:test", "network:test"]) { + group = 'Tests' + description = 'Run all JVM tests' +} + +task robolectricTests(type: Exec) { + group = 'Tests' + description = 'Run all Robolectric tests based on the Instrumented naming convention' + commandLine 'sh', './gradlew', 'testDebugUnitTest', '--tests', 'org.fnives.test.*InstrumentedTest' +} + +task androidTests(dependsOn: ["app:connectedDebugAndroidTest"]) { + group = 'Tests' + description = 'Run Android tests' } \ No newline at end of file From a69fdce26c3bd033c038a37bb5d0cbf6bbf9a263 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 14:51:15 +0200 Subject: [PATCH 11/17] Issue#49 Add first integration test to core --- core/build.gradle | 2 + .../core/content/GetAllContentUseCase.kt | 2 + .../integration/ContentIntegrationTest.kt | 364 ++++++++++++++++++ .../fake/FakeFavouriteContentLocalStorage.kt | 30 ++ .../fake/FakeUserDataLocalStorage.kt | 6 + .../core/testutil/AwaitElementEmitCount.kt | 24 ++ 6 files changed, 428 insertions(+) create mode 100644 core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt create mode 100644 core/src/test/java/org/fnives/test/showcase/core/integration/fake/FakeFavouriteContentLocalStorage.kt create mode 100644 core/src/test/java/org/fnives/test/showcase/core/integration/fake/FakeUserDataLocalStorage.kt create mode 100644 core/src/test/java/org/fnives/test/showcase/core/testutil/AwaitElementEmitCount.kt diff --git a/core/build.gradle b/core/build.gradle index e2b19e3..3ca195f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -26,4 +26,6 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version" testImplementation "app.cash.turbine:turbine:$turbine_version" + + testImplementation project(':mockserver') } \ No newline at end of file diff --git a/core/src/main/java/org/fnives/test/showcase/core/content/GetAllContentUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/content/GetAllContentUseCase.kt index 24a4b63..7487d94 100644 --- a/core/src/main/java/org/fnives/test/showcase/core/content/GetAllContentUseCase.kt +++ b/core/src/main/java/org/fnives/test/showcase/core/content/GetAllContentUseCase.kt @@ -2,6 +2,7 @@ package org.fnives.test.showcase.core.content import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage import org.fnives.test.showcase.model.content.Content import org.fnives.test.showcase.model.content.ContentId @@ -18,6 +19,7 @@ class GetAllContentUseCase internal constructor( favouriteContentLocalStorage.observeFavourites(), ::combineContentWithFavourites ) + .distinctUntilChanged() companion object { private fun combineContentWithFavourites( diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt new file mode 100644 index 0000000..00c280d --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt @@ -0,0 +1,364 @@ +package org.fnives.test.showcase.core.integration + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase +import org.fnives.test.showcase.core.content.FetchContentUseCase +import org.fnives.test.showcase.core.content.GetAllContentUseCase +import org.fnives.test.showcase.core.content.RemoveContentFromFavouritesUseCase +import org.fnives.test.showcase.core.di.koin.createCoreModule +import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage +import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage +import org.fnives.test.showcase.core.session.SessionExpirationListener +import org.fnives.test.showcase.core.testutil.AwaitElementEmitCount +import org.fnives.test.showcase.model.content.ContentId +import org.fnives.test.showcase.model.content.FavouriteContent +import org.fnives.test.showcase.model.network.BaseUrl +import org.fnives.test.showcase.model.session.Session +import org.fnives.test.showcase.model.shared.Resource +import org.fnives.test.showcase.network.mockserver.ContentData +import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup +import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario +import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario +import org.fnives.test.showcase.network.shared.exceptions.NetworkException +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.test.KoinTest +import org.koin.test.inject +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyZeroInteractions + +@OptIn(ExperimentalCoroutinesApi::class) +class ContentIntegrationTest : KoinTest { + + private lateinit var mockServerScenarioSetup: MockServerScenarioSetup + private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage + private lateinit var mockSessionExpirationListener: SessionExpirationListener + private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage + private val addContentToFavouriteUseCase by inject() + private val fetchContentUseCase by inject() + private val getAllContentUseCase by inject() + private val removeContentFromFavouritesUseCase by inject() + private val session = Session(accessToken = "login-access", refreshToken = "login-refresh") + + @BeforeEach + fun setup() { + mockSessionExpirationListener = mock() + mockServerScenarioSetup = MockServerScenarioSetup() + val url = mockServerScenarioSetup.start(false) + fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage() + fakeUserDataLocalStorage = FakeUserDataLocalStorage(session) + + startKoin { + modules( + createCoreModule( + baseUrl = BaseUrl(url), + enableNetworkLogging = true, + favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage }, + sessionExpirationListenerProvider = { mockSessionExpirationListener }, + userDataLocalStorageProvider = { fakeUserDataLocalStorage } + ).toList() + ) + } + } + + @AfterEach + fun tearDown() { + stopKoin() + mockServerScenarioSetup.stop() + } + + @DisplayName("GIVEN normal response without favourites WHEN observed THEN data is returned") + @Test + fun withoutFavouritesDataIsReturned() = runTest { + mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false)) + val contentData = ContentData.contentSuccess.map { FavouriteContent(it, false) } + val expected = listOf( + Resource.Loading(), + Resource.Success(contentData) + ) + + val actual = async { + getAllContentUseCase.get() + .take(2) + .toList() + } + + Assertions.assertEquals(expected, actual.await()) + verifyZeroInteractions(mockSessionExpirationListener) + Assertions.assertSame(session, fakeUserDataLocalStorage.session) + } + + @DisplayName("GIVEN normal response without favourites matching WHEN observed THEN data is returned") + @Test + fun withoutFavouritesMatchingDataIsReturned() = runTest { + mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false)) + addContentToFavouriteUseCase.invoke(ContentId("non-existent-content-id")) + advanceUntilIdle() + val contentData = ContentData.contentSuccess.map { FavouriteContent(it, false) } + val expected = listOf( + Resource.Loading(), + Resource.Success(contentData) + ) + + val actual = async { + getAllContentUseCase.get() + .take(2) + .toList() + } + + Assertions.assertEquals(expected, actual.await()) + verifyZeroInteractions(mockSessionExpirationListener) + Assertions.assertSame(session, fakeUserDataLocalStorage.session) + } + + @DisplayName("GIVEN normal response without favourites matching WHEN observed loading and modifying favourites THEN no extra loading is emitted") + @Test + fun modifyingFavouritesWhileLoadingDoesntEmitNewValue() = runTest { + mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false)) + addContentToFavouriteUseCase.invoke(ContentId("non-existent-content-id")) + advanceUntilIdle() + val contentData = ContentData.contentSuccess.mapIndexed { index, it -> + FavouriteContent(it, index == 0) + } + val expected = listOf( + Resource.Loading(), + Resource.Success(contentData) + ) + + val actual = async { + getAllContentUseCase.get() + .onEach { + if (it is Resource.Loading) { + addContentToFavouriteUseCase.invoke(contentData.first().content.id) + } + } + .take(2) + .toList() + } + + Assertions.assertEquals(expected, actual.await()) + verifyZeroInteractions(mockSessionExpirationListener) + Assertions.assertSame(session, fakeUserDataLocalStorage.session) + } + + @DisplayName("GIVEN normal response without favourites WHEN adding favourite and removing THEN we get proper updates") + @Test + fun addingRemoving() = runTest { + mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false)) + + val startContentData = ContentData.contentSuccess.map { + FavouriteContent(it, isFavourite = false) + } + val addedFavouriteData = startContentData.mapIndexed { index, it -> + if (index == 0) it.copy(isFavourite = true) else it + } + val added2ndFavouriteData = addedFavouriteData.mapIndexed { index, it -> + if (index == 1) it.copy(isFavourite = true) else it + } + val removedFirstFavouriteData = added2ndFavouriteData.mapIndexed { index, it -> + if (index == 0) it.copy(isFavourite = false) else it + } + val expected = listOf( + Resource.Loading(), + Resource.Success(startContentData), + Resource.Success(addedFavouriteData), + Resource.Success(added2ndFavouriteData), + Resource.Success(removedFirstFavouriteData) + ) + + val actual = async { + getAllContentUseCase.get() + .take(5) + .toList() + } + getAllContentUseCase.get().take(2).toList() // let's await success request + + addContentToFavouriteUseCase.invoke(startContentData.first().content.id) + advanceUntilIdle() + addContentToFavouriteUseCase.invoke(startContentData.drop(1).first().content.id) + advanceUntilIdle() + removeContentFromFavouritesUseCase.invoke(startContentData.first().content.id) + advanceUntilIdle() + + val verifyCaching = async { + getAllContentUseCase.get().take(1).first() + } + + Assertions.assertIterableEquals(expected, actual.await()) + Assertions.assertEquals(expected.last(), verifyCaching.await()) + verifyZeroInteractions(mockSessionExpirationListener) + Assertions.assertSame(session, fakeUserDataLocalStorage.session) + } + + @DisplayName("GIVEN normal response with favourites WHEN getting the data THEN we get proper updates") + @Test + fun alreadySavedFavourites() = runTest { + mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false)) + addContentToFavouriteUseCase.invoke(ContentData.contentSuccess.first().id) + addContentToFavouriteUseCase.invoke(ContentData.contentSuccess.takeLast(1).first().id) + val favouritedIndexes = listOf(0, ContentData.contentSuccess.size - 1) + + val expectedContents = ContentData.contentSuccess.mapIndexed { index, content -> + FavouriteContent(content, favouritedIndexes.contains(index)) + } + + val expected = listOf( + Resource.Loading(), + Resource.Success(expectedContents), + ) + + val actual = async { + getAllContentUseCase.get() + .take(2) + .toList() + } + + Assertions.assertIterableEquals(expected, actual.await()) + verifyZeroInteractions(mockSessionExpirationListener) + Assertions.assertSame(session, fakeUserDataLocalStorage.session) + } + + @DisplayName("GIVEN error response WHEN fetching THEN the data is received") + @Test + fun errorFetch() = runTest { + mockServerScenarioSetup.setScenario( + ContentScenario.Error(usingRefreshedToken = false) + .then(ContentScenario.Success(usingRefreshedToken = false)) + ) + + val expectedContents = ContentData.contentSuccess.map { content -> + FavouriteContent(content, false) + } + val expected = listOf( + Resource.Loading(), + Resource.Error(mock()), + Resource.Loading(), + Resource.Success(expectedContents), + ) + + val awaitElementEmitionCount = AwaitElementEmitCount(2) + val actual = async { + getAllContentUseCase.get() + .take(4) + .let(awaitElementEmitionCount::attach) + .toList() + } + awaitElementEmitionCount.await() // await 2 emissions, aka the request to finish + + fetchContentUseCase.invoke() + + val actualValues = actual.await() + Assertions.assertEquals(expected[0], actualValues[0]) + Assertions.assertTrue(actualValues[1] is Resource.Error, "Resource is Error") + Assertions.assertTrue((actualValues[1] as Resource.Error).error is NetworkException, "Resource is Network Error") + Assertions.assertEquals(expected[2], actualValues[2]) + Assertions.assertEquals(expected[3], actualValues[3]) + verifyZeroInteractions(mockSessionExpirationListener) + Assertions.assertSame(session, fakeUserDataLocalStorage.session) + } + + @DisplayName("GIVEN proper response WHEN fetching THEN the data is received") + @Test + fun fetchingAgain() = runTest { + mockServerScenarioSetup.setScenario( + ContentScenario.Success(usingRefreshedToken = false) + .then(ContentScenario.SuccessWithMissingFields(usingRefreshedToken = false)) + ) + + val expectedContents = ContentData.contentSuccess.map { content -> + FavouriteContent(content, false) + } + val expectedContents2 = ContentData.contentSuccessWithMissingFields.map { content -> + FavouriteContent(content, false) + } + val expected = listOf( + Resource.Loading(), + Resource.Success(expectedContents), + Resource.Loading(), + Resource.Success(expectedContents2), + ) + + val awaitElementEmitionCount = AwaitElementEmitCount(2) + val actual = async { + getAllContentUseCase.get() + .take(4) + .let(awaitElementEmitionCount::attach) + .toList() + } + awaitElementEmitionCount.await() // await 2 emissions, aka the request to finish + + fetchContentUseCase.invoke() + + Assertions.assertIterableEquals(expected, actual.await()) + } + + @DisplayName("GIVEN session expiration then proper response WHEN observing THEN the data is received") + @Test + fun sessionRefreshing() = runTest { + mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success) + .setScenario( + ContentScenario.Unauthorized(usingRefreshedToken = false) + .then(ContentScenario.Success(usingRefreshedToken = true)) + ) + + val expectedContents = ContentData.contentSuccess.map { content -> + FavouriteContent(content, false) + } + val expected = listOf( + Resource.Loading(), + Resource.Success(expectedContents) + ) + + val actual = async { + getAllContentUseCase.get() + .take(2) + .toList() + } + + Assertions.assertIterableEquals(expected, actual.await()) + verifyZeroInteractions(mockSessionExpirationListener) + val expectedSession = Session(accessToken = "refreshed-access", refreshToken = "refreshed-refresh") + Assertions.assertEquals(expectedSession, fakeUserDataLocalStorage.session) + } + + @DisplayName("GIVEN session expiration and failing token-refresh response WHEN observing THEN session expiration is attached") + @Test + fun sessionExpiration() = runTest { + mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error) + .setScenario( + ContentScenario.Unauthorized(usingRefreshedToken = false) + .then(ContentScenario.Success(usingRefreshedToken = true)) + ) + + val actual = async { + getAllContentUseCase.get() + .take(2) + .toList() + } + + val actualValues = actual.await() + Assertions.assertEquals(Resource.Loading>(), actualValues[0]) + Assertions.assertTrue(actualValues[1] is Resource.Error, "Resource is Error") + Assertions.assertTrue((actualValues[1] as Resource.Error).error is NetworkException, "Resource is Network Error") + + verify(mockSessionExpirationListener, times(1)).onSessionExpired() + verifyNoMoreInteractions(mockSessionExpirationListener) + + Assertions.assertEquals(null, fakeUserDataLocalStorage.session) + } +} diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/fake/FakeFavouriteContentLocalStorage.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/FakeFavouriteContentLocalStorage.kt new file mode 100644 index 0000000..a420c27 --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/FakeFavouriteContentLocalStorage.kt @@ -0,0 +1,30 @@ +package org.fnives.test.showcase.core.integration.fake + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage +import org.fnives.test.showcase.model.content.ContentId + +class FakeFavouriteContentLocalStorage : FavouriteContentLocalStorage { + + private val dataFlow = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + init { + dataFlow.tryEmit(emptyList()) + } + + override fun observeFavourites(): Flow> = dataFlow.asSharedFlow() + + override suspend fun markAsFavourite(contentId: ContentId) { + dataFlow.emit(dataFlow.replayCache.first().plus(contentId)) + } + + override suspend fun deleteAsFavourite(contentId: ContentId) { + dataFlow.emit(dataFlow.replayCache.first().minus(contentId)) + } +} diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/fake/FakeUserDataLocalStorage.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/FakeUserDataLocalStorage.kt new file mode 100644 index 0000000..a7a66a0 --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/FakeUserDataLocalStorage.kt @@ -0,0 +1,6 @@ +package org.fnives.test.showcase.core.integration.fake + +import org.fnives.test.showcase.core.storage.UserDataLocalStorage +import org.fnives.test.showcase.model.session.Session + +class FakeUserDataLocalStorage(override var session: Session? = null) : UserDataLocalStorage diff --git a/core/src/test/java/org/fnives/test/showcase/core/testutil/AwaitElementEmitCount.kt b/core/src/test/java/org/fnives/test/showcase/core/testutil/AwaitElementEmitCount.kt new file mode 100644 index 0000000..fee7332 --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/testutil/AwaitElementEmitCount.kt @@ -0,0 +1,24 @@ +package org.fnives.test.showcase.core.testutil + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach + +class AwaitElementEmitCount(private var counter: Int) { + + private val completableDeferred = CompletableDeferred() + + init { + assert(counter > 0) + } + + fun attach(flow: Flow): Flow = + flow.onEach { + counter-- + if (counter == 0) { + completableDeferred.complete(Unit) + } + } + + suspend fun await() = completableDeferred.await() +} From 555ad6d05f7500e3e8ede3349d88a6480d614df3 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 20:48:03 +0200 Subject: [PATCH 12/17] Issue#49 Add core-network integration tests --- core/build.gradle | 2 + .../core/integration/AuthIntegrationTest.kt | 167 ++++++++++++++++++ .../integration/ContentIntegrationTest.kt | 29 --- .../SessionExpirationIntegrationTest.kt | 117 ++++++++++++ 4 files changed, 286 insertions(+), 29 deletions(-) create mode 100644 core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt create mode 100644 core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt diff --git a/core/build.gradle b/core/build.gradle index 3ca195f..9a43f9a 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -27,5 +27,7 @@ dependencies { testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version" testImplementation "app.cash.turbine:turbine:$turbine_version" + testImplementation "org.junit.jupiter:junit-jupiter-params:$testing_junit5_version" + testImplementation project(':mockserver') } \ No newline at end of file diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt new file mode 100644 index 0000000..41a2b77 --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt @@ -0,0 +1,167 @@ +package org.fnives.test.showcase.core.integration + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.fnives.test.showcase.core.di.koin.createCoreModule +import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage +import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage +import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase +import org.fnives.test.showcase.core.login.LoginUseCase +import org.fnives.test.showcase.core.login.LogoutUseCase +import org.fnives.test.showcase.core.session.SessionExpirationListener +import org.fnives.test.showcase.model.auth.LoginCredentials +import org.fnives.test.showcase.model.auth.LoginStatus +import org.fnives.test.showcase.model.network.BaseUrl +import org.fnives.test.showcase.model.shared.Answer +import org.fnives.test.showcase.network.mockserver.ContentData +import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup +import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.test.KoinTest +import org.koin.test.inject +import org.mockito.kotlin.mock +import org.mockito.kotlin.verifyZeroInteractions +import java.util.stream.Stream + +@OptIn(ExperimentalCoroutinesApi::class) +class AuthIntegrationTest : KoinTest { + + private lateinit var mockServerScenarioSetup: MockServerScenarioSetup + private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage + private lateinit var mockSessionExpirationListener: SessionExpirationListener + private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage + private val isUserLoggedInUseCase by inject() + private val loginUseCase by inject() + private val logoutUseCase by inject() + + @BeforeEach + fun setup() { + mockSessionExpirationListener = mock() + mockServerScenarioSetup = MockServerScenarioSetup() + val url = mockServerScenarioSetup.start(false) + fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage() + fakeUserDataLocalStorage = FakeUserDataLocalStorage(null) + + startKoin { + modules( + createCoreModule( + baseUrl = BaseUrl(url), + enableNetworkLogging = true, + favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage }, + sessionExpirationListenerProvider = { mockSessionExpirationListener }, + userDataLocalStorageProvider = { fakeUserDataLocalStorage } + ).toList() + ) + } + } + + @AfterEach + fun tearDown() { + mockServerScenarioSetup.stop() + stopKoin() + } + + @DisplayName("GIVEN no session saved WHEN checking if user is logged in THEN they are not") + @Test + fun withoutSessionTheUserIsNotLoggedIn() = runTest { + fakeUserDataLocalStorage.session = null + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertFalse(actual, "User is expected to be not logged in") + verifyZeroInteractions(mockSessionExpirationListener) + } + + @DisplayName("GIVEN no session WHEN user is logging in THEN they get session") + @Test + fun login() = runTest { + mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) + val expectedSession = ContentData.loginSuccessResponse + + val answer = loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertEquals(Answer.Success(LoginStatus.SUCCESS), answer) + Assertions.assertTrue(actual, "User is expected to be logged in") + Assertions.assertEquals(expectedSession, fakeUserDataLocalStorage.session) + verifyZeroInteractions(mockSessionExpirationListener) + } + + @MethodSource("localInputErrorArguments") + @ParameterizedTest(name = "GIVEN {0} credentials WHEN login called THEN error {1} is shown") + fun localInputError(credentials: LoginCredentials, loginError: LoginStatus) = runTest { + val answer = loginUseCase.invoke(credentials) + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertEquals(Answer.Success(loginError), answer) + Assertions.assertFalse(actual, "User is expected to be not logged in") + Assertions.assertEquals(null, fakeUserDataLocalStorage.session) + verifyZeroInteractions(mockSessionExpirationListener) + } + + @DisplayName("GIVEN no session WHEN user is logging in THEN they get session") + @Test + fun loginInvalidCredentials() = runTest { + mockServerScenarioSetup.setScenario(AuthScenario.InvalidCredentials(username = "usr", password = "sEc"), validateArguments = true) + + val answer = loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertEquals(Answer.Success(LoginStatus.INVALID_CREDENTIALS), answer) + Assertions.assertFalse(actual, "User is expected to be not logged in") + Assertions.assertEquals(null, fakeUserDataLocalStorage.session) + verifyZeroInteractions(mockSessionExpirationListener) + } + + @MethodSource("networkErrorArguments") + @ParameterizedTest(name = "GIVEN {0} network response WHEN login called THEN error is shown") + fun networkInputError(authScenario: AuthScenario) = runTest { + mockServerScenarioSetup.setScenario(authScenario, validateArguments = true) + val credentials = LoginCredentials(username = authScenario.username, password = authScenario.password) + val answer = loginUseCase.invoke(credentials) + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertTrue(answer is Answer.Error, "Answer is expected to be an Error") + Assertions.assertFalse(actual, "User is expected to be not logged in") + Assertions.assertEquals(null, fakeUserDataLocalStorage.session) + verifyZeroInteractions(mockSessionExpirationListener) + } + + @DisplayName("GIVEN logged in user WHEN user is login out THEN they no longer have a session and content is cleared") + @Test + fun logout() = runTest { + mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) + loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) + + logoutUseCase.invoke() + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertEquals(false, actual, "User is expected to be logged out") + verifyZeroInteractions(mockSessionExpirationListener) + } + + companion object { + + @JvmStatic + fun localInputErrorArguments() = Stream.of( + Arguments.of(LoginCredentials("", "password"), LoginStatus.INVALID_USERNAME), + Arguments.of(LoginCredentials("username", ""), LoginStatus.INVALID_PASSWORD) + ) + + @JvmStatic + fun networkErrorArguments() = Stream.of( + Arguments.of(AuthScenario.GenericError(username = "a", password = "b")), + Arguments.of(AuthScenario.UnexpectedJsonAsSuccessResponse(username = "a", password = "b")), + Arguments.of(AuthScenario.MalformedJsonAsSuccessResponse(username = "a", password = "b")), + Arguments.of(AuthScenario.MissingFieldJson(username = "a", password = "b")) + ) + } +} diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt index 00c280d..adc9e15 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt @@ -37,9 +37,6 @@ import org.koin.core.context.stopKoin import org.koin.test.KoinTest import org.koin.test.inject import org.mockito.kotlin.mock -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.verifyZeroInteractions @OptIn(ExperimentalCoroutinesApi::class) @@ -335,30 +332,4 @@ class ContentIntegrationTest : KoinTest { val expectedSession = Session(accessToken = "refreshed-access", refreshToken = "refreshed-refresh") Assertions.assertEquals(expectedSession, fakeUserDataLocalStorage.session) } - - @DisplayName("GIVEN session expiration and failing token-refresh response WHEN observing THEN session expiration is attached") - @Test - fun sessionExpiration() = runTest { - mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error) - .setScenario( - ContentScenario.Unauthorized(usingRefreshedToken = false) - .then(ContentScenario.Success(usingRefreshedToken = true)) - ) - - val actual = async { - getAllContentUseCase.get() - .take(2) - .toList() - } - - val actualValues = actual.await() - Assertions.assertEquals(Resource.Loading>(), actualValues[0]) - Assertions.assertTrue(actualValues[1] is Resource.Error, "Resource is Error") - Assertions.assertTrue((actualValues[1] as Resource.Error).error is NetworkException, "Resource is Network Error") - - verify(mockSessionExpirationListener, times(1)).onSessionExpired() - verifyNoMoreInteractions(mockSessionExpirationListener) - - Assertions.assertEquals(null, fakeUserDataLocalStorage.session) - } } diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt new file mode 100644 index 0000000..6417136 --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt @@ -0,0 +1,117 @@ +package org.fnives.test.showcase.core.integration + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import org.fnives.test.showcase.core.content.FetchContentUseCase +import org.fnives.test.showcase.core.content.GetAllContentUseCase +import org.fnives.test.showcase.core.di.koin.createCoreModule +import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage +import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage +import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase +import org.fnives.test.showcase.core.login.LoginUseCase +import org.fnives.test.showcase.core.session.SessionExpirationListener +import org.fnives.test.showcase.model.auth.LoginCredentials +import org.fnives.test.showcase.model.network.BaseUrl +import org.fnives.test.showcase.model.shared.Resource +import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup +import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario +import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario +import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario +import org.fnives.test.showcase.network.shared.exceptions.NetworkException +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.test.KoinTest +import org.koin.test.inject +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyZeroInteractions + +@OptIn(ExperimentalCoroutinesApi::class) +class SessionExpirationIntegrationTest : KoinTest { + + private lateinit var mockServerScenarioSetup: MockServerScenarioSetup + private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage + private lateinit var mockSessionExpirationListener: SessionExpirationListener + private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage + private val isUserLoggedInUseCase by inject() + private val getAllContentUseCase by inject() + private val loginUseCase by inject() + private val fetchContentUseCase by inject() + + @BeforeEach + fun setup() { + mockSessionExpirationListener = mock() + mockServerScenarioSetup = MockServerScenarioSetup() + val url = mockServerScenarioSetup.start(false) + fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage() + fakeUserDataLocalStorage = FakeUserDataLocalStorage(null) + + startKoin { + modules( + createCoreModule( + baseUrl = BaseUrl(url), + enableNetworkLogging = true, + favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage }, + sessionExpirationListenerProvider = { mockSessionExpirationListener }, + userDataLocalStorageProvider = { fakeUserDataLocalStorage } + ).toList() + ) + } + } + + @AfterEach + fun tearDown() { + mockServerScenarioSetup.stop() + stopKoin() + } + + @DisplayName("GIVEN logged in user WHEN fetching but expired THEN user is logged out") + @Test + fun sessionResultsInErrorAndClearsContent() = runTest { + mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"), validateArguments = true) + loginUseCase.invoke(LoginCredentials(username = "a", password = "b")) + Assertions.assertTrue(isUserLoggedInUseCase.invoke()) + verifyZeroInteractions(mockSessionExpirationListener) + + mockServerScenarioSetup.setScenario(ContentScenario.Unauthorized(usingRefreshedToken = false)) + .setScenario(RefreshTokenScenario.Error) + + getAllContentUseCase.get().take(2).toList() // getting session expiration + + verify(mockSessionExpirationListener, times(1)).onSessionExpired() + verifyNoMoreInteractions(mockSessionExpirationListener) + Assertions.assertFalse(isUserLoggedInUseCase.invoke(), "User is expected to be logged out") + } + + @DisplayName("GIVEN session expiration and failing token-refresh response WHEN requiring data THEN error is returned and data is cleared") + @Test + fun sessionExpirationResultsInLogout() = runTest { + mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "", password = ""), validateArguments = true) + loginUseCase.invoke(LoginCredentials(username = "", password = "")) + + mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error) + .setScenario( + ContentScenario.Success(usingRefreshedToken = true) + .then(ContentScenario.Unauthorized(usingRefreshedToken = false)) + .then(ContentScenario.Success(usingRefreshedToken = true)) + ) + + getAllContentUseCase.get().take(2).toList() // cachedData + + fetchContentUseCase.invoke() + val unauthorizedData = getAllContentUseCase.get().take(2).last() + + Assertions.assertTrue(unauthorizedData is Resource.Error, "Resource is Error") + Assertions.assertTrue((unauthorizedData as Resource.Error).error is NetworkException, "Resource is Network Error") + } +} From 3f4d22528cb4a3638541d68f777bfe1285d3a42d Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 22:46:03 +0200 Subject: [PATCH 13/17] Issue#8 Take use of TestFixtures to keep MockWebServer TLS setup in the network module, while still being able to use it in Android Tests --- app/build.gradle | 3 + ...MockServerScenarioSetupResetingTestRule.kt | 32 +-------- .../idling/NetworkSynchronizationTestRule.kt | 19 ++---- build.gradle | 2 +- core/build.gradle | 2 + .../core/integration/AuthIntegrationTest.kt | 13 ++-- .../integration/ContentIntegrationTest.kt | 13 ++-- .../SessionExpirationIntegrationTest.kt | 13 ++-- gradle/wrapper/gradle-wrapper.properties | 6 +- gradlescripts/testoptions.gradle | 1 + network/build.gradle | 8 ++- .../network/di/createNetworkmodules.kt | 4 +- .../LoginRemoteSourceRefreshActionImplTest.kt | 2 +- .../network/auth/LoginRemoteSourceTest.kt | 2 +- .../content/ContentRemoteSourceImplTest.kt | 2 +- .../network/content/SessionExpirationTest.kt | 2 +- .../MockServerScenarioSetupExtensions.kt | 2 +- .../NetworkTestConfigurationHelper.kt | 68 +++++++++++++++++++ 18 files changed, 117 insertions(+), 77 deletions(-) rename network/src/{test/java/org/fnives/test/showcase/network/shared => testFixtures/java/org/fnives/test/showcase/network/testutil}/MockServerScenarioSetupExtensions.kt (93%) create mode 100644 network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/NetworkTestConfigurationHelper.kt diff --git a/app/build.gradle b/app/build.gradle index 163f056..e27ddf4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,5 +121,8 @@ dependencies { androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version" implementation "io.reactivex.rxjava3:rxjava:3.1.3" + + testImplementation testFixtures(project(':core')) + androidTestImplementation testFixtures(project(':core')) } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt index 72b48fe..421eb28 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/MockServerScenarioSetupResetingTestRule.kt @@ -1,18 +1,12 @@ package org.fnives.test.showcase.testutils -import okhttp3.OkHttpClient -import okhttp3.tls.HandshakeCertificates -import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup +import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper import org.fnives.test.showcase.testutils.idling.NetworkSynchronizationTestRule -import org.fnives.test.showcase.testutils.idling.NetworkSynchronizationTestRule.OkHttpClientTypes import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import org.koin.core.context.loadKoinModules -import org.koin.dsl.module import org.koin.test.KoinTest -import org.koin.test.get /** * TestRule which ensures Koin is reseted between each tests and setups Network mocking. @@ -28,8 +22,6 @@ class MockServerScenarioSetupResetingTestRule( lateinit var mockServerScenarioSetup: MockServerScenarioSetup - private val sessionlessQualifier get() = OkHttpClientTypes.SESSIONLESS.asQualifier() - override fun apply(base: Statement, description: Description): Statement = networkSynchronizationTestRule.apply(base, description) .let(::createStatement) @@ -48,29 +40,9 @@ class MockServerScenarioSetupResetingTestRule( } private fun before() { - mockServerScenarioSetup = MockServerScenarioSetup() - val url = mockServerScenarioSetup.start(true) - - val handshakeCertificates = mockServerScenarioSetup.clientCertificates - ?: throw IllegalStateException("ClientCertificate should be accessable") - - val okHttpClientWithCertificate = createUpdateOkHttpClient(handshakeCertificates) - - loadKoinModules( - module { - // add https certificate to okhttp - single(qualifier = sessionlessQualifier) { okHttpClientWithCertificate } - // replace base url with mockWebServer's - single { BaseUrl(url) } - } - ) + mockServerScenarioSetup = NetworkTestConfigurationHelper.startWithHTTPSMockWebServer() } - private fun createUpdateOkHttpClient(handshakeCertificates: HandshakeCertificates) = - get(sessionlessQualifier).newBuilder() - .sslSocketFactory(handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager) - .build() - private fun after() { mockServerScenarioSetup.stop() } diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt index dc97866..d2cf705 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronizationTestRule.kt @@ -3,12 +3,11 @@ package org.fnives.test.showcase.testutils.idling import androidx.annotation.CheckResult import androidx.test.espresso.IdlingResource import okhttp3.OkHttpClient +import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import org.koin.core.qualifier.StringQualifier import org.koin.test.KoinTest -import org.koin.test.get class NetworkSynchronizationTestRule : TestRule, KoinTest { @@ -31,24 +30,14 @@ class NetworkSynchronizationTestRule : TestRule, KoinTest { @CheckResult private fun registerNetworkingSynchronization(): Disposable { - val idlingResources = OkHttpClientTypes.values() - .map { it to getOkHttpClient(it) } - .associateBy { it.second.dispatcher } - .values - .map { (key, client) -> client.asIdlingResource(key.qualifier) } + val idlingResources = NetworkTestConfigurationHelper.getOkHttpClients() + .associateBy(keySelector = { it.toString() }) + .map { (key, client) -> client.asIdlingResource(key) } .map(::IdlingResourceDisposable) return CompositeDisposable(idlingResources) } - private fun getOkHttpClient(type: OkHttpClientTypes): OkHttpClient = get(type.asQualifier()) - private fun OkHttpClient.asIdlingResource(name: String): IdlingResource = OkHttp3IdlingResource.create(name, this) - - enum class OkHttpClientTypes(val qualifier: String) { - SESSION("SESSION-NETWORKING"), SESSIONLESS("SESSIONLESS-NETWORKING"); - - fun asQualifier() = StringQualifier(qualifier) - } } diff --git a/build.gradle b/build.gradle index 9c84311..508e9e8 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1" } diff --git a/core/build.gradle b/core/build.gradle index 9a43f9a..617bb40 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java-library' id 'kotlin' id 'kotlin-kapt' + id 'java-test-fixtures' } java { @@ -30,4 +31,5 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-params:$testing_junit5_version" testImplementation project(':mockserver') + testFixturesApi testFixtures(project(':network')) } \ No newline at end of file diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt index 41a2b77..c1afe1d 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt @@ -14,13 +14,14 @@ import org.fnives.test.showcase.model.auth.LoginStatus import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.model.shared.Answer import org.fnives.test.showcase.network.mockserver.ContentData -import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource @@ -35,7 +36,10 @@ import java.util.stream.Stream @OptIn(ExperimentalCoroutinesApi::class) class AuthIntegrationTest : KoinTest { - private lateinit var mockServerScenarioSetup: MockServerScenarioSetup + @RegisterExtension + @JvmField + val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions() + private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage private lateinit var mockSessionExpirationListener: SessionExpirationListener private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage @@ -46,15 +50,13 @@ class AuthIntegrationTest : KoinTest { @BeforeEach fun setup() { mockSessionExpirationListener = mock() - mockServerScenarioSetup = MockServerScenarioSetup() - val url = mockServerScenarioSetup.start(false) fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage() fakeUserDataLocalStorage = FakeUserDataLocalStorage(null) startKoin { modules( createCoreModule( - baseUrl = BaseUrl(url), + baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url), enableNetworkLogging = true, favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage }, sessionExpirationListenerProvider = { mockSessionExpirationListener }, @@ -66,7 +68,6 @@ class AuthIntegrationTest : KoinTest { @AfterEach fun tearDown() { - mockServerScenarioSetup.stop() stopKoin() } diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt index adc9e15..068ed08 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt @@ -23,15 +23,16 @@ import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.model.session.Session import org.fnives.test.showcase.model.shared.Resource import org.fnives.test.showcase.network.mockserver.ContentData -import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.test.KoinTest @@ -42,7 +43,10 @@ import org.mockito.kotlin.verifyZeroInteractions @OptIn(ExperimentalCoroutinesApi::class) class ContentIntegrationTest : KoinTest { - private lateinit var mockServerScenarioSetup: MockServerScenarioSetup + @RegisterExtension + @JvmField + val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions() + private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage private lateinit var mockSessionExpirationListener: SessionExpirationListener private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage @@ -55,15 +59,13 @@ class ContentIntegrationTest : KoinTest { @BeforeEach fun setup() { mockSessionExpirationListener = mock() - mockServerScenarioSetup = MockServerScenarioSetup() - val url = mockServerScenarioSetup.start(false) fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage() fakeUserDataLocalStorage = FakeUserDataLocalStorage(session) startKoin { modules( createCoreModule( - baseUrl = BaseUrl(url), + baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url), enableNetworkLogging = true, favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage }, sessionExpirationListenerProvider = { mockSessionExpirationListener }, @@ -76,7 +78,6 @@ class ContentIntegrationTest : KoinTest { @AfterEach fun tearDown() { stopKoin() - mockServerScenarioSetup.stop() } @DisplayName("GIVEN normal response without favourites WHEN observed THEN data is returned") diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt index 6417136..6dcca45 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt @@ -16,16 +16,17 @@ import org.fnives.test.showcase.core.session.SessionExpirationListener import org.fnives.test.showcase.model.auth.LoginCredentials import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.model.shared.Resource -import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.network.shared.exceptions.NetworkException +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.test.KoinTest @@ -39,7 +40,10 @@ import org.mockito.kotlin.verifyZeroInteractions @OptIn(ExperimentalCoroutinesApi::class) class SessionExpirationIntegrationTest : KoinTest { - private lateinit var mockServerScenarioSetup: MockServerScenarioSetup + @RegisterExtension + @JvmField + val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions() + private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage private lateinit var mockSessionExpirationListener: SessionExpirationListener private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage @@ -51,15 +55,13 @@ class SessionExpirationIntegrationTest : KoinTest { @BeforeEach fun setup() { mockSessionExpirationListener = mock() - mockServerScenarioSetup = MockServerScenarioSetup() - val url = mockServerScenarioSetup.start(false) fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage() fakeUserDataLocalStorage = FakeUserDataLocalStorage(null) startKoin { modules( createCoreModule( - baseUrl = BaseUrl(url), + baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url), enableNetworkLogging = true, favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage }, sessionExpirationListenerProvider = { mockSessionExpirationListener }, @@ -71,7 +73,6 @@ class SessionExpirationIntegrationTest : KoinTest { @AfterEach fun tearDown() { - mockServerScenarioSetup.stop() stopKoin() } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4ee106d..804c205 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Apr 11 21:03:49 EEST 2021 +#Thu Jan 27 21:44:07 EET 2022 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlescripts/testoptions.gradle b/gradlescripts/testoptions.gradle index 9c0e171..ce6575a 100644 --- a/gradlescripts/testoptions.gradle +++ b/gradlescripts/testoptions.gradle @@ -72,6 +72,7 @@ task jvmTests(dependsOn: ["app:testDebugUnitTest", "core:test", "network:test"]) task robolectricTests(type: Exec) { group = 'Tests' description = 'Run all Robolectric tests based on the Instrumented naming convention' + // todo is there a better way? commandLine 'sh', './gradlew', 'testDebugUnitTest', '--tests', 'org.fnives.test.*InstrumentedTest' } diff --git a/network/build.gradle b/network/build.gradle index cbd2de4..e8cf19c 100644 --- a/network/build.gradle +++ b/network/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java-library' id 'kotlin' id 'kotlin-kapt' + id 'java-test-fixtures' } java { @@ -25,9 +26,10 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version" - testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" - testImplementation project(':mockserver') - testImplementation "io.insert-koin:koin-test-junit5:$koin_version" testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" + + testFixturesApi project(':mockserver') + testFixturesApi "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version" + testFixturesApi "io.insert-koin:koin-test-junit5:$koin_version" } \ No newline at end of file diff --git a/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt b/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt index 019faa7..974390a 100644 --- a/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt +++ b/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt @@ -88,5 +88,5 @@ private fun sessionNetworkingModule( single(qualifier = session) { get(sessionless).newBuilder().client(get(session)).build() } } -private val session = StringQualifier("SESSION-NETWORKING") -private val sessionless = StringQualifier("SESSIONLESS-NETWORKING") +internal val session = StringQualifier("SESSION-NETWORKING") +internal val sessionless = StringQualifier("SESSIONLESS-NETWORKING") diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt index dc982fe..a3e4270 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt @@ -6,7 +6,7 @@ import org.fnives.test.showcase.network.di.createNetworkModules import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage -import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException import org.fnives.test.showcase.network.shared.exceptions.ParsingException import org.junit.jupiter.api.AfterEach diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt index 0075cf9..cd0925e 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt @@ -11,7 +11,7 @@ import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.ContentData.createExpectedLoginRequestJson import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage -import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException import org.fnives.test.showcase.network.shared.exceptions.ParsingException import org.junit.jupiter.api.AfterEach diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt index 9fe16ff..fe5c9f5 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt @@ -6,7 +6,7 @@ import org.fnives.test.showcase.network.di.createNetworkModules import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage -import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException import org.fnives.test.showcase.network.shared.exceptions.ParsingException import org.junit.jupiter.api.AfterEach diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt index c4ee725..cffd7f5 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt @@ -9,7 +9,7 @@ import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScena import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage -import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions diff --git a/network/src/test/java/org/fnives/test/showcase/network/shared/MockServerScenarioSetupExtensions.kt b/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/MockServerScenarioSetupExtensions.kt similarity index 93% rename from network/src/test/java/org/fnives/test/showcase/network/shared/MockServerScenarioSetupExtensions.kt rename to network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/MockServerScenarioSetupExtensions.kt index 6c6fd7d..5a1c9dd 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/shared/MockServerScenarioSetupExtensions.kt +++ b/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/MockServerScenarioSetupExtensions.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.network.shared +package org.fnives.test.showcase.network.testutil import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup import org.junit.jupiter.api.extension.AfterEachCallback diff --git a/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/NetworkTestConfigurationHelper.kt b/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/NetworkTestConfigurationHelper.kt new file mode 100644 index 0000000..7c54124 --- /dev/null +++ b/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/NetworkTestConfigurationHelper.kt @@ -0,0 +1,68 @@ +package org.fnives.test.showcase.network.testutil + +import okhttp3.OkHttpClient +import org.fnives.test.showcase.model.network.BaseUrl +import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup +import org.koin.core.context.loadKoinModules +import org.koin.core.qualifier.StringQualifier +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.get + +/** + * Gives access to internals of Networking so it can be used in MockWebServer more easily. + */ +object NetworkTestConfigurationHelper : KoinTest { + + /** + * For some reason importing these didn't work. Still keeping internal, cause it shouldn't leave the module. + * + * import org.fnives.test.showcase.network.di.session + * import org.fnives.test.showcase.network.di.sessionless + */ + internal val session = StringQualifier("SESSION-NETWORKING") + internal val sessionless = StringQualifier("SESSIONLESS-NETWORKING") + + /** + * After koin started, this gives you access for the OkHttpClients, so you can synchronize or keep track of them + */ + fun getOkHttpClients(): List = listOf( + get(sessionless), + get(session) + ) + + /** + * After koin started, this sets up MockServer to be used with HTTPs. + * + * Url, and injected OkHttpClient is modified for this. + */ + fun startWithHTTPSMockWebServer(): MockServerScenarioSetup{ + val mockServerScenarioSetup = MockServerScenarioSetup() + val url = mockServerScenarioSetup.start(true) + + val handshakeCertificates = mockServerScenarioSetup.clientCertificates + ?: throw IllegalStateException("ClientCertificate should be accessable") + + reload(baseUrl = BaseUrl(url)) { + it.newBuilder() + .sslSocketFactory(handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager) + .build() + } + + return mockServerScenarioSetup + } + + private fun reload(baseUrl: BaseUrl, adjustments: (OkHttpClient) -> OkHttpClient) { + val current = get(sessionless) + + val adjusted = adjustments(current) + loadKoinModules( + module { + // add https certificate to okhttp + single(qualifier = sessionless) { adjusted } + // replace base url with mockWebServer's + single { baseUrl } + } + ) + } +} \ No newline at end of file From 9a63cdba380242598b000308a49644463c7dc05a Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Thu, 27 Jan 2022 22:46:46 +0200 Subject: [PATCH 14/17] Fix file location --- .../main/java/org/fnives/test/showcase/di/createAppModules.kt | 2 +- .../java/org/fnives/test/showcase/core/di/createCoreModule.kt | 2 +- .../java/org/fnives/test/showcase/core/login/LogoutUseCase.kt | 2 +- .../test/showcase/core/integration/AuthIntegrationTest.kt | 2 +- .../test/showcase/core/integration/ContentIntegrationTest.kt | 2 +- .../core/integration/SessionExpirationIntegrationTest.kt | 2 +- .../org/fnives/test/showcase/core/login/LogoutUseCaseTest.kt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/fnives/test/showcase/di/createAppModules.kt b/app/src/main/java/org/fnives/test/showcase/di/createAppModules.kt index 4c20221..8d05715 100644 --- a/app/src/main/java/org/fnives/test/showcase/di/createAppModules.kt +++ b/app/src/main/java/org/fnives/test/showcase/di/createAppModules.kt @@ -1,6 +1,6 @@ package org.fnives.test.showcase.di -import org.fnives.test.showcase.core.di.koin.createCoreModule +import org.fnives.test.showcase.core.di.createCoreModule import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.session.SessionExpirationListenerImpl import org.fnives.test.showcase.storage.LocalDatabase diff --git a/core/src/main/java/org/fnives/test/showcase/core/di/createCoreModule.kt b/core/src/main/java/org/fnives/test/showcase/core/di/createCoreModule.kt index 721b26f..d3b6bc5 100644 --- a/core/src/main/java/org/fnives/test/showcase/core/di/createCoreModule.kt +++ b/core/src/main/java/org/fnives/test/showcase/core/di/createCoreModule.kt @@ -1,4 +1,4 @@ -package org.fnives.test.showcase.core.di.koin +package org.fnives.test.showcase.core.di import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase import org.fnives.test.showcase.core.content.ContentRepository diff --git a/core/src/main/java/org/fnives/test/showcase/core/login/LogoutUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/login/LogoutUseCase.kt index 961a666..d40d268 100644 --- a/core/src/main/java/org/fnives/test/showcase/core/login/LogoutUseCase.kt +++ b/core/src/main/java/org/fnives/test/showcase/core/login/LogoutUseCase.kt @@ -1,6 +1,6 @@ package org.fnives.test.showcase.core.login -import org.fnives.test.showcase.core.di.koin.repositoryModule +import org.fnives.test.showcase.core.di.repositoryModule import org.fnives.test.showcase.core.storage.UserDataLocalStorage import org.koin.core.context.loadKoinModules diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt index c1afe1d..88129df 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt @@ -2,7 +2,7 @@ package org.fnives.test.showcase.core.integration import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import org.fnives.test.showcase.core.di.koin.createCoreModule +import org.fnives.test.showcase.core.di.createCoreModule import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt index 068ed08..42798a8 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt @@ -12,7 +12,7 @@ import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase import org.fnives.test.showcase.core.content.FetchContentUseCase import org.fnives.test.showcase.core.content.GetAllContentUseCase import org.fnives.test.showcase.core.content.RemoveContentFromFavouritesUseCase -import org.fnives.test.showcase.core.di.koin.createCoreModule +import org.fnives.test.showcase.core.di.createCoreModule import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage import org.fnives.test.showcase.core.session.SessionExpirationListener diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt index 6dcca45..5ef2ada 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/SessionExpirationIntegrationTest.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import org.fnives.test.showcase.core.content.FetchContentUseCase import org.fnives.test.showcase.core.content.GetAllContentUseCase -import org.fnives.test.showcase.core.di.koin.createCoreModule +import org.fnives.test.showcase.core.di.createCoreModule import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase diff --git a/core/src/test/java/org/fnives/test/showcase/core/login/LogoutUseCaseTest.kt b/core/src/test/java/org/fnives/test/showcase/core/login/LogoutUseCaseTest.kt index 230ee1b..32f697e 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/login/LogoutUseCaseTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/login/LogoutUseCaseTest.kt @@ -3,7 +3,7 @@ package org.fnives.test.showcase.core.login import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.fnives.test.showcase.core.content.ContentRepository -import org.fnives.test.showcase.core.di.koin.createCoreModule +import org.fnives.test.showcase.core.di.createCoreModule import org.fnives.test.showcase.core.storage.UserDataLocalStorage import org.fnives.test.showcase.model.network.BaseUrl import org.junit.jupiter.api.AfterEach From 24cffc50577eca7e79b2e411e7da3078f44b5429 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Fri, 28 Jan 2022 00:24:22 +0200 Subject: [PATCH 15/17] Issue#49 Add Integration Test instruction set for Core --- README.md | 12 +- codekata/core.again.instructionset.md | 334 ++++++++++++++++++ codekata/robolectric.instructionset.md | 2 +- .../core/integration/AuthIntegrationTest.kt | 62 +++- .../CodeKataAuthIntegrationTest.kt | 101 ++++++ .../CodeKataFavouriteContentLocalStorage.kt | 4 + .../fake/CodeKataUserDataLocalStorage.kt | 4 + 7 files changed, 495 insertions(+), 24 deletions(-) create mode 100644 codekata/core.again.instructionset.md create mode 100644 core/src/test/java/org/fnives/test/showcase/core/integration/CodeKataAuthIntegrationTest.kt create mode 100644 core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataFavouriteContentLocalStorage.kt create mode 100644 core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataUserDataLocalStorage.kt diff --git a/README.md b/README.md index 1655a23..7e86c90 100644 --- a/README.md +++ b/README.md @@ -115,18 +115,16 @@ The actual server when running the application is [mockapi.io](https://www.mocka Download the project, open it in [Android Studio](https://developer.android.com/studio?gclid=Cj0KCQjw1PSDBhDbARIsAPeTqrfKrSx8qD_B9FegOmpVgxtPWFHhBHeqnml8n4ak-I5wPvqlwGdwrUQaAtobEALw_wcB&gclsrc=aw.ds). * In the gradle window you can see in the root gradle there is a "tests" group. In this group you will see a unitTests and androidTests task. -* First run the unitTests. +* First run the jvmTests. * When that finished, build the application to your phone. * Login with whatever credentials and look over the app, what will you test. * When finished, run androidTests. This will ensure the testing setup is proper, the project can resolve all the dependencies and such issues won't come up during your exercise. -If everything is right, change branch to codeKata and look for into the [codekata](./codekata) folder for the instruction sets. - ### Structure -The Code Kata is structured into 5 different section, each section in different what we are testing and how we are testing it. +The Code Kata is structured into 6 different section, each section in different what we are testing and how we are testing it. Since our layering is "app", "core" and "networking", of course we will jump right into the middle and start with core. @@ -153,6 +151,12 @@ We will also see how to test with LiveData. We will introduce Rules, aka easy to reuse "Before" and "After" components. +#### Core Again +Open the [core again instruction set](./codekata/core.again.instructionset.md). + +We complicate things here. We write our first Integraiton Test. +We will verify the Authentication classes and the networking module is working together like a charm. + #### App Robolectric Unit Tests. Open the [app robolectric unit tests instruction set](./codekata/robolectric.instructionset.md). diff --git a/codekata/core.again.instructionset.md b/codekata/core.again.instructionset.md new file mode 100644 index 0000000..7a9adc1 --- /dev/null +++ b/codekata/core.again.instructionset.md @@ -0,0 +1,334 @@ +# 4. Starting of integration testing + +You probably got bored of Unit Testing if you got to this point, so let's switch it up a little. + +In this testing instruction set you will learn how to write simple Integration tests for your Java module: + +- How to write integration tests +- How to use Fakes +- How to depend on test modules +- Exercise parametrized tests +- Exercise Junit Extensions + +## AuthIntegrationTest test + +Our System Under Test will be all Authentication related public classes of Core module, so namely: + - `org.fnives.test.showcase.core.login.IsUserLoggedInUseCase` + - `org.fnives.test.showcase.core.login.LoginUseCase` + - `org.fnives.test.showcase.core.login.LogoutUseCase` + +What we want to test here, is that all components hidden behind these classes together let the user login, store their session and logout. + +### Setup + +So let's open up our test class: `org.fnives.test.showcase.core.integration.CodeKataAuthIntegrationTest` + +First, we want to take advantage of our DI module, so let's inject our actual classes: +```kotlin +private val isUserLoggedInUseCase by inject() +private val loginUseCase by inject() +private val logoutUseCase by inject() +``` + +Now let's startKoin in our setup method: +```kotlin +@BeforeEach +fun setup() { + startKoin { + modules( + createCoreModule( + baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url), + enableNetworkLogging = true, + favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage }, + sessionExpirationListenerProvider = { mockSessionExpirationListener }, + userDataLocalStorageProvider = { fakeUserDataLocalStorage } + ).toList() + ) + } +} + +@AfterEach +fun tearDown() { + stopKoin() +} +``` + +Okay, a couple of things are missing. First of what are those fakes? Let's start with them + +#### Fakes + +So the `FavouriteContentLocalStorage` and `UserDataLocalStorage` will be injected into our modules. +However, we expect a specific behaviour from them. + +So instead of mocking them, let's create simple fakes, that we can use in our tests, as they were the real class. + +Let's start with `FakeUserDataLocalStorage`. + +###### Let's open `CodeKataUserDataLocalStorage`. + +This has to extend the `UserDataLocalStorage` interface, so add that. And the only required implementation is a modifiable field. So add it as a constructor argument and that's it. + +```kotlin +class CodeKataUserDataLocalStorage(override var session: Session? = null) : UserDataLocalStorage +``` + +###### Now let's open `CodeKataFavouriteContentLocalStorage`. + +This is a bit more tricky, there are multiple methods. + +First of all we need a flow, so let's just use a SharedFlow and initialize it: +```kotlin +private val dataFlow = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, +) + +init { + dataFlow.tryEmit(emptyList()) +} +``` + +With that we can return our flow from `observeFavourites`: + +```kotlin +override fun observeFavourites(): Flow> = dataFlow.asSharedFlow() +``` + +And our methods just need to update the flow as it would be expected: +```kotlin +override suspend fun markAsFavourite(contentId: ContentId) { + dataFlow.emit(dataFlow.replayCache.first().plus(contentId)) +} + +override suspend fun deleteAsFavourite(contentId: ContentId) { + dataFlow.emit(dataFlow.replayCache.first().minus(contentId)) +} +``` + +Okay, we have our fakes. Let's navigate back to `CodeKataAuthIntegrationTest` + +#### Continue Setup + +Let's just declare our fakes and initialize them in the setup: +```kotlin +private lateinit var fakeFavouriteContentLocalStorage: FavouriteContentLocalStorage +private lateinit var mockSessionExpirationListener: SessionExpirationListener +private lateinit var fakeUserDataLocalStorage: UserDataLocalStorage + +@Before +fun setup() { + mockSessionExpirationListener = mock() // we are using mock, since it only has 1 function so we just want to verify if it's called + fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage() + fakeUserDataLocalStorage = FakeUserDataLocalStorage(null) + startKoin { + ///... + } +} +``` + +We are still missing `mockServerScenarioSetupExtensions` this will be our TestExtension, to initialize MockWebServer. +`MockServerScenarioSetupExtensions` is declared in the `:network` test module. +However we are still able to import it. + +That's because of [java-test-fixtures](https://docs.gradle.org/current/userguide/java_testing.html#sec:java_test_fixtures) plugin. It is can be used to depend on a specific test module "textFixtures". +Check out the build.gradle's to see how that's done. +This can be useful to share some static Test Data, or extensions in our case. + +So let's add this extension: +```kotlin +@RegisterExtension +@JvmField +val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions() +private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup +``` + +This extension is a wrapper around MockWebServer containing setups of requests, request verifications and ContentData. +It is useful to mock our requests with this extension from now on so we don't repeat ourselves. + +With that let's start testing: + +### 1. `withoutSessionTheUserIsNotLoggedIn` + +As usual, we start with the simplest test. Let's verify that if the session object is null, we are indeed logged out: + +```kotlin +@DisplayName("GIVEN no session saved WHEN checking if user is logged in THEN they are not") + @Test + fun withoutSessionTheUserIsNotLoggedIn() = runTest { + fakeUserDataLocalStorage.session = null + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertFalse(actual, "User is expected to be not logged in") + verifyZeroInteractions(mockSessionExpirationListener) +``` + +### 2. `loginSuccess` + +Let's test that given good credentials and success response, our user can login in. + +First we setup our mock server and the expected session: +```kotlin +mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) // validate arguments just verifies the request path, body, headers etc. +val expectedSession = ContentData.loginSuccessResponse +``` + +Now we login, and then check if we are actually logged in: +```kotlin +val answer = loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) +val actual = isUserLoggedInUseCase.invoke() +``` + +And just verify: +```kotlin +Assertions.assertEquals(Answer.Success(LoginStatus.SUCCESS), answer) +Assertions.assertTrue(actual, "User is expected to be logged in") +Assertions.assertEquals(expectedSession, fakeUserDataLocalStorage.session) +verifyZeroInteractions(mockSessionExpirationListener) +``` + +With this, looks like our Integration works correctly. Requests are called, proper response is received, login state is changed. + +### 3. `localInputError` +We have to expected errors, that are returned even before running requests, if the username or password is empty. +This two tests would be really similar, so let's do Parametrized tests. + +First we modify our method signature: +```kotlin +@MethodSource("localInputErrorArguments") +@ParameterizedTest(name = "GIVEN {0} credentials WHEN login called THEN error {1} is shown") +fun localInputError(credentials: LoginCredentials, loginError: LoginStatus) +``` + +Now let's declare our action: +```kotlin +val answer = loginUseCase.invoke(credentials) +val actual = isUserLoggedInUseCase.invoke() +``` + +And do our verifications, aka not logged in, not session expired and the correct error: +```kotlin +Assertions.assertEquals(Answer.Success(loginError), answer) +Assertions.assertFalse(actual, "User is expected to be not logged in") +Assertions.assertEquals(null, fakeUserDataLocalStorage.session) +verifyZeroInteractions(mockSessionExpirationListener) +``` + +Now we just need to declare our parameters for our test: +```kotlin +companion object { + + @JvmStatic + fun localInputErrorArguments() = Stream.of( + Arguments.of(LoginCredentials("", "password"), LoginStatus.INVALID_USERNAME), + Arguments.of(LoginCredentials("username", ""), LoginStatus.INVALID_PASSWORD) + ) +} +``` + +With that we covered both of these errors. + +### 4. `networkInputError` + +Now let's do the same with network inputs. This will be really similar, only difference is we will initialize our mockserver with the AuthScenario. +Try to do it yourself, however for completeness sake, as usual, here is the code: +```kotlin +@MethodSource("networkErrorArguments") +@ParameterizedTest(name = "GIVEN {0} network response WHEN login called THEN error is shown") +fun networkInputError(authScenario: AuthScenario) = runTest { + mockServerScenarioSetup.setScenario(authScenario, validateArguments = true) + val credentials = LoginCredentials(username = authScenario.username, password = authScenario.password) + val answer = loginUseCase.invoke(credentials) + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertTrue(answer is Answer.Error, "Answer is expected to be an Error") + Assertions.assertFalse(actual, "User is expected to be not logged in") + Assertions.assertEquals(null, fakeUserDataLocalStorage.session) + verifyZeroInteractions(mockSessionExpirationListener) +} +``` + +### 5. `loginInvalidCredentials` + +We have one more expected error type, but this comes from the NetworkResponse. We could add it as parametrized test, but for the sake of readability, let's just keep it separate. + +Thi is really similar to the `networkInputError`, the differences are that this is not parametrized, we use `AuthScenario.InvalidCredentials` response and we expect `Answer.Success(LoginStatus.INVALID_CREDENTIALS)` + +So together: +```kotlin +@DisplayName("GIVEN no session WHEN user is logging in THEN they get session") +@Test +fun loginInvalidCredentials() = runTest { + mockServerScenarioSetup.setScenario(AuthScenario.InvalidCredentials(username = "usr", password = "sEc"), validateArguments = true) + + val answer = loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertEquals(Answer.Success(LoginStatus.INVALID_CREDENTIALS), answer) + Assertions.assertFalse(actual, "User is expected to be not logged in") + Assertions.assertEquals(null, fakeUserDataLocalStorage.session) + verifyZeroInteractions(mockSessionExpirationListener) +} +``` + +### 6. `logout` +Now let's verify if the user can logout properly. + +For this we first need to have the user in a logged in state: +```kotlin +mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) +loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) +``` + +The user needs to logout: +```kotlin +logoutUseCase.invoke() +val actual = isUserLoggedInUseCase.invoke() +``` + +And we verify the user is indeed logged out now: +```kotlin +Assertions.assertFalse(actual, "User is expected to be logged out") +Assertions.assertEquals(null, fakeUserDataLocalStorage.session) +verifyZeroInteractions(mockSessionExpirationListener) +``` + +### 7. `logoutReleasesContent` +At last, let's verify that when the user logs out, their cache is released and the request is no longer authenticated. + +To do this, first we setup our MockServer and login the user: +```kotlin +mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) + .setScenario(ContentScenario.Success(usingRefreshedToken = false), validateArguments = true) +loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) +``` + +Now we get the content values before and after logout: +```kotlin +val valuesBeforeLogout = get().get().take(2).last() +logoutUseCase.invoke() +val valuesAfterLogout = get().get().take(2).last() +``` +> Note: we are using get() from koin, since we don't want to depend on how the data is cleared and this way we get the UseCase a new user would get. + +Now there is a bit of explaining to do. How `mockServerScenarioSetup` is setup is that if `validateArguments` is set, it will verify the path, the body and the authentication token. If it doesn't match, it will return a BAD Request. +We could do the same with MockWebServer and recorded request as well, it's just now hidden behind our TestHelper MockServer. + +So what we want to verify, is that `valuesBeforeLogout` is a success, and the `valuesAfterLogout` is a failure. + +```kotlin +Assertions.assertTrue(valuesBeforeLogout is Resource.Success, "Before we expect a cached Success") +Assertions.assertTrue(valuesAfterLogout is Resource.Error, "After we expect an error, since our request no longer is authenticated") +``` +If it would be cached, the test would be stuck, cause Loading wouldn't be emitted, or if the request would be authenticated success would be returned as we setup Success response. + +## Conclusions +With that we wrote our Integration tests. +There is no point of going over other integration test's in the core module, since the idea is captured, and nothing new could be shown. +If you want to give it a go, feel free, however consider using turbine for flow tests, cause it can be a bit tricky. + +What we have learned: +- In integration tests, we mock the least amount of classes +- In integration tests we verify multiple classes and how they work together +- We learned we can share test classes between modules +- We learned how to write fakes +- We exercised the Parametrized tests diff --git a/codekata/robolectric.instructionset.md b/codekata/robolectric.instructionset.md index 1889fad..7dcf81f 100644 --- a/codekata/robolectric.instructionset.md +++ b/codekata/robolectric.instructionset.md @@ -1,4 +1,4 @@ -# 3. Starting of Robolectric testing +# 5. Starting of Robolectric testing So we are finally here, so far we didn't had to touch any kind of context or resources, activities, fragments or anything android. This is where we have to get back to reality and actually deal with Android. diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt index 88129df..6d31dc5 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/AuthIntegrationTest.kt @@ -1,7 +1,10 @@ package org.fnives.test.showcase.core.integration import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.take import kotlinx.coroutines.test.runTest +import org.fnives.test.showcase.core.content.GetAllContentUseCase import org.fnives.test.showcase.core.di.createCoreModule import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage @@ -9,12 +12,16 @@ import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase import org.fnives.test.showcase.core.login.LoginUseCase import org.fnives.test.showcase.core.login.LogoutUseCase import org.fnives.test.showcase.core.session.SessionExpirationListener +import org.fnives.test.showcase.core.storage.UserDataLocalStorage +import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage import org.fnives.test.showcase.model.auth.LoginCredentials import org.fnives.test.showcase.model.auth.LoginStatus import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.model.shared.Answer +import org.fnives.test.showcase.model.shared.Resource import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario +import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions @@ -28,6 +35,7 @@ import org.junit.jupiter.params.provider.MethodSource import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.test.KoinTest +import org.koin.test.get import org.koin.test.inject import org.mockito.kotlin.mock import org.mockito.kotlin.verifyZeroInteractions @@ -40,9 +48,9 @@ class AuthIntegrationTest : KoinTest { @JvmField val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions() private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup - private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage + private lateinit var fakeFavouriteContentLocalStorage: FavouriteContentLocalStorage private lateinit var mockSessionExpirationListener: SessionExpirationListener - private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage + private lateinit var fakeUserDataLocalStorage: UserDataLocalStorage private val isUserLoggedInUseCase by inject() private val loginUseCase by inject() private val logoutUseCase by inject() @@ -83,7 +91,7 @@ class AuthIntegrationTest : KoinTest { @DisplayName("GIVEN no session WHEN user is logging in THEN they get session") @Test - fun login() = runTest { + fun loginSuccess() = runTest { mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) val expectedSession = ContentData.loginSuccessResponse @@ -108,20 +116,6 @@ class AuthIntegrationTest : KoinTest { verifyZeroInteractions(mockSessionExpirationListener) } - @DisplayName("GIVEN no session WHEN user is logging in THEN they get session") - @Test - fun loginInvalidCredentials() = runTest { - mockServerScenarioSetup.setScenario(AuthScenario.InvalidCredentials(username = "usr", password = "sEc"), validateArguments = true) - - val answer = loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) - val actual = isUserLoggedInUseCase.invoke() - - Assertions.assertEquals(Answer.Success(LoginStatus.INVALID_CREDENTIALS), answer) - Assertions.assertFalse(actual, "User is expected to be not logged in") - Assertions.assertEquals(null, fakeUserDataLocalStorage.session) - verifyZeroInteractions(mockSessionExpirationListener) - } - @MethodSource("networkErrorArguments") @ParameterizedTest(name = "GIVEN {0} network response WHEN login called THEN error is shown") fun networkInputError(authScenario: AuthScenario) = runTest { @@ -136,7 +130,21 @@ class AuthIntegrationTest : KoinTest { verifyZeroInteractions(mockSessionExpirationListener) } - @DisplayName("GIVEN logged in user WHEN user is login out THEN they no longer have a session and content is cleared") + @DisplayName("GIVEN no session WHEN user is logging in THEN they get session") + @Test + fun loginInvalidCredentials() = runTest { + mockServerScenarioSetup.setScenario(AuthScenario.InvalidCredentials(username = "usr", password = "sEc"), validateArguments = true) + + val answer = loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) + val actual = isUserLoggedInUseCase.invoke() + + Assertions.assertEquals(Answer.Success(LoginStatus.INVALID_CREDENTIALS), answer) + Assertions.assertFalse(actual, "User is expected to be not logged in") + Assertions.assertEquals(null, fakeUserDataLocalStorage.session) + verifyZeroInteractions(mockSessionExpirationListener) + } + + @DisplayName("GIVEN logged in user WHEN user is login out THEN they no longer have a session") @Test fun logout() = runTest { mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) @@ -145,10 +153,26 @@ class AuthIntegrationTest : KoinTest { logoutUseCase.invoke() val actual = isUserLoggedInUseCase.invoke() - Assertions.assertEquals(false, actual, "User is expected to be logged out") + Assertions.assertFalse(actual, "User is expected to be logged out") + Assertions.assertEquals(null, fakeUserDataLocalStorage.session) verifyZeroInteractions(mockSessionExpirationListener) } + @DisplayName("GIVEN logged in user WHEN user is login out THEN content is cleared") + @Test + fun logoutReleasesContent() = runTest { + mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) + .setScenario(ContentScenario.Success(usingRefreshedToken = false), validateArguments = true) + loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc")) + + val valuesBeforeLogout = get().get().take(2).last() + logoutUseCase.invoke() + val valuesAfterLogout = get().get().take(2).last() + + Assertions.assertTrue(valuesBeforeLogout is Resource.Success, "Before we expect a cached Success") + Assertions.assertTrue(valuesAfterLogout is Resource.Error, "After we expect an error, since our request no longer is authenticated") + } + companion object { @JvmStatic diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/CodeKataAuthIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/CodeKataAuthIntegrationTest.kt new file mode 100644 index 0000000..3907e7f --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/CodeKataAuthIntegrationTest.kt @@ -0,0 +1,101 @@ +package org.fnives.test.showcase.core.integration + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.fnives.test.showcase.core.di.createCoreModule +import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage +import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage +import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase +import org.fnives.test.showcase.core.login.LoginUseCase +import org.fnives.test.showcase.core.login.LogoutUseCase +import org.fnives.test.showcase.core.session.SessionExpirationListener +import org.fnives.test.showcase.core.storage.UserDataLocalStorage +import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage +import org.fnives.test.showcase.model.auth.LoginCredentials +import org.fnives.test.showcase.model.auth.LoginStatus +import org.fnives.test.showcase.model.network.BaseUrl +import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.koin.core.context.GlobalContext.startKoin +import org.koin.core.context.GlobalContext.stopKoin +import org.koin.test.KoinTest +import org.koin.test.inject +import org.mockito.kotlin.mock + +@OptIn(ExperimentalCoroutinesApi::class) +@Disabled("CodeKata") +class CodeKataAuthIntegrationTest : KoinTest { + + @RegisterExtension + @JvmField + val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions() + private val isUserLoggedInUseCase by inject() + private val loginUseCase by inject() + private val logoutUseCase by inject() + private lateinit var fakeFavouriteContentLocalStorage: FavouriteContentLocalStorage + private lateinit var mockSessionExpirationListener: SessionExpirationListener + private lateinit var fakeUserDataLocalStorage: UserDataLocalStorage + + @BeforeEach + fun setup() { + mockSessionExpirationListener = mock() // we are using mock, since it only has 1 function so we just want to verify if it's called + fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage() + fakeUserDataLocalStorage = FakeUserDataLocalStorage(null) + startKoin { + modules( + createCoreModule( + baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url), + enableNetworkLogging = true, + favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage }, + sessionExpirationListenerProvider = { mockSessionExpirationListener }, + userDataLocalStorageProvider = { fakeUserDataLocalStorage } + ).toList() + ) + } + } + + @AfterEach + fun tearDown() { + stopKoin() + } + + @DisplayName("GIVEN no session saved WHEN checking if user is logged in THEN they are not") + @Test + fun withoutSessionTheUserIsNotLoggedIn() = runTest { + } + + @DisplayName("GIVEN no session WHEN user is logging in THEN they get session") + @Test + fun loginSuccess() = runTest { + } + + @DisplayName("GIVEN credentials WHEN login called THEN error is shown") + @Test + fun localInputError(credentials: LoginCredentials, loginError: LoginStatus) = runTest { + } + + @DisplayName("GIVEN network response WHEN login called THEN error is shown") + @Test + fun networkInputError(authScenario: AuthScenario) = runTest { + } + + @DisplayName("GIVEN no session WHEN user is logging in THEN they get session") + @Test + fun loginInvalidCredentials() = runTest { + } + + @DisplayName("GIVEN logged in user WHEN user is login out THEN they no longer have a session and content is cleared") + @Test + fun logout() = runTest { + } + @DisplayName("GIVEN logged in user WHEN user is login out THEN content is cleared") + @Test + fun logoutReleasesContent() = runTest { + } +} diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataFavouriteContentLocalStorage.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataFavouriteContentLocalStorage.kt new file mode 100644 index 0000000..ae064eb --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataFavouriteContentLocalStorage.kt @@ -0,0 +1,4 @@ +package org.fnives.test.showcase.core.integration.fake + +class CodeKataFavouriteContentLocalStorage { +} diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataUserDataLocalStorage.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataUserDataLocalStorage.kt new file mode 100644 index 0000000..ff9b779 --- /dev/null +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataUserDataLocalStorage.kt @@ -0,0 +1,4 @@ +package org.fnives.test.showcase.core.integration.fake + +class CodeKataUserDataLocalStorage { +} From 85faee64ea8d84c288e81e34fb53800d09de0440 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Fri, 28 Jan 2022 00:33:48 +0200 Subject: [PATCH 16/17] Fix code analysis errors --- app/src/main/AndroidManifest.xml | 2 +- .../test/showcase/core/integration/ContentIntegrationTest.kt | 2 +- .../integration/fake/CodeKataFavouriteContentLocalStorage.kt | 3 +-- .../core/integration/fake/CodeKataUserDataLocalStorage.kt | 3 +-- .../network/auth/LoginRemoteSourceRefreshActionImplTest.kt | 2 +- .../test/showcase/network/auth/LoginRemoteSourceTest.kt | 2 +- .../showcase/network/content/ContentRemoteSourceImplTest.kt | 2 +- .../test/showcase/network/content/SessionExpirationTest.kt | 2 +- .../network/testutil/NetworkTestConfigurationHelper.kt | 4 ++-- 9 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f64d7bd..92f5a5e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.TestShowCase" - tools:ignore="AllowBackup"> + tools:ignore="AllowBackup,DataExtractionRules"> diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt index 42798a8..934566c 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/ContentIntegrationTest.kt @@ -25,8 +25,8 @@ import org.fnives.test.showcase.model.shared.Resource import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario -import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataFavouriteContentLocalStorage.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataFavouriteContentLocalStorage.kt index ae064eb..07c1df3 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataFavouriteContentLocalStorage.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataFavouriteContentLocalStorage.kt @@ -1,4 +1,3 @@ package org.fnives.test.showcase.core.integration.fake -class CodeKataFavouriteContentLocalStorage { -} +class CodeKataFavouriteContentLocalStorage diff --git a/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataUserDataLocalStorage.kt b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataUserDataLocalStorage.kt index ff9b779..474831a 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataUserDataLocalStorage.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/integration/fake/CodeKataUserDataLocalStorage.kt @@ -1,4 +1,3 @@ package org.fnives.test.showcase.core.integration.fake -class CodeKataUserDataLocalStorage { -} +class CodeKataUserDataLocalStorage diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt index a3e4270..51f6ebd 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt @@ -6,9 +6,9 @@ import org.fnives.test.showcase.network.di.createNetworkModules import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage -import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException import org.fnives.test.showcase.network.shared.exceptions.ParsingException +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt index cd0925e..08cfc39 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt @@ -11,9 +11,9 @@ import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.ContentData.createExpectedLoginRequestJson import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage -import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException import org.fnives.test.showcase.network.shared.exceptions.ParsingException +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt index fe5c9f5..21d2001 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt @@ -6,9 +6,9 @@ import org.fnives.test.showcase.network.di.createNetworkModules import org.fnives.test.showcase.network.mockserver.ContentData import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage -import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException import org.fnives.test.showcase.network.shared.exceptions.ParsingException +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt index cffd7f5..bc9fda7 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt @@ -9,8 +9,8 @@ import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScena import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage -import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.fnives.test.showcase.network.shared.exceptions.NetworkException +import org.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach diff --git a/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/NetworkTestConfigurationHelper.kt b/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/NetworkTestConfigurationHelper.kt index 7c54124..ddcff23 100644 --- a/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/NetworkTestConfigurationHelper.kt +++ b/network/src/testFixtures/java/org/fnives/test/showcase/network/testutil/NetworkTestConfigurationHelper.kt @@ -36,7 +36,7 @@ object NetworkTestConfigurationHelper : KoinTest { * * Url, and injected OkHttpClient is modified for this. */ - fun startWithHTTPSMockWebServer(): MockServerScenarioSetup{ + fun startWithHTTPSMockWebServer(): MockServerScenarioSetup { val mockServerScenarioSetup = MockServerScenarioSetup() val url = mockServerScenarioSetup.start(true) @@ -65,4 +65,4 @@ object NetworkTestConfigurationHelper : KoinTest { } ) } -} \ No newline at end of file +} From 222774bc6f3662e2c78f0b7795499aee05f6d890 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Fri, 28 Jan 2022 00:38:03 +0200 Subject: [PATCH 17/17] Fix GitHub integration with tests --- .github/workflows/pull-request-jobs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request-jobs.yml b/.github/workflows/pull-request-jobs.yml index 2b4d0af..82508f3 100644 --- a/.github/workflows/pull-request-jobs.yml +++ b/.github/workflows/pull-request-jobs.yml @@ -65,11 +65,11 @@ jobs: distribution: 'adopt' java-version: '11' - name: Run Unit Tests - run: ./gradlew unitTests + run: ./gradlew jvmTests - name: Upload Test Results uses: actions/upload-artifact@v2 if: always() with: - name: Unit Test Results + name: JVM Test Results path: ./**/build/reports/tests/**/index.html retention-days: 1