Issue#49 Add Integration Test instruction set for Core
This commit is contained in:
parent
9a63cdba38
commit
24cffc5057
7 changed files with 495 additions and 24 deletions
12
README.md
12
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).
|
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.
|
* 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.
|
* When that finished, build the application to your phone.
|
||||||
* Login with whatever credentials and look over the app, what will you test.
|
* Login with whatever credentials and look over the app, what will you test.
|
||||||
* When finished, run androidTests.
|
* 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.
|
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
|
### 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.
|
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.
|
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.
|
#### App Robolectric Unit Tests.
|
||||||
Open the [app robolectric unit tests instruction set](./codekata/robolectric.instructionset.md).
|
Open the [app robolectric unit tests instruction set](./codekata/robolectric.instructionset.md).
|
||||||
|
|
||||||
|
|
|
||||||
334
codekata/core.again.instructionset.md
Normal file
334
codekata/core.again.instructionset.md
Normal file
|
|
@ -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<IsUserLoggedInUseCase>()
|
||||||
|
private val loginUseCase by inject<LoginUseCase>()
|
||||||
|
private val logoutUseCase by inject<LogoutUseCase>()
|
||||||
|
```
|
||||||
|
|
||||||
|
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<List<ContentId>>(
|
||||||
|
replay = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
dataFlow.tryEmit(emptyList())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With that we can return our flow from `observeFavourites`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun observeFavourites(): Flow<List<ContentId>> = 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<GetAllContentUseCase>().get().take(2).last()
|
||||||
|
logoutUseCase.invoke()
|
||||||
|
val valuesAfterLogout = get<GetAllContentUseCase>().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
|
||||||
|
|
@ -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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package org.fnives.test.showcase.core.integration
|
package org.fnives.test.showcase.core.integration
|
||||||
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.last
|
||||||
|
import kotlinx.coroutines.flow.take
|
||||||
import kotlinx.coroutines.test.runTest
|
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.di.createCoreModule
|
||||||
import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage
|
import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage
|
||||||
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
|
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.LoginUseCase
|
||||||
import org.fnives.test.showcase.core.login.LogoutUseCase
|
import org.fnives.test.showcase.core.login.LogoutUseCase
|
||||||
import org.fnives.test.showcase.core.session.SessionExpirationListener
|
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.LoginCredentials
|
||||||
import org.fnives.test.showcase.model.auth.LoginStatus
|
import org.fnives.test.showcase.model.auth.LoginStatus
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
import org.fnives.test.showcase.model.network.BaseUrl
|
||||||
import org.fnives.test.showcase.model.shared.Answer
|
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.ContentData
|
||||||
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
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.fnives.test.showcase.network.testutil.MockServerScenarioSetupExtensions
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Assertions
|
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.startKoin
|
||||||
import org.koin.core.context.stopKoin
|
import org.koin.core.context.stopKoin
|
||||||
import org.koin.test.KoinTest
|
import org.koin.test.KoinTest
|
||||||
|
import org.koin.test.get
|
||||||
import org.koin.test.inject
|
import org.koin.test.inject
|
||||||
import org.mockito.kotlin.mock
|
import org.mockito.kotlin.mock
|
||||||
import org.mockito.kotlin.verifyZeroInteractions
|
import org.mockito.kotlin.verifyZeroInteractions
|
||||||
|
|
@ -40,9 +48,9 @@ class AuthIntegrationTest : KoinTest {
|
||||||
@JvmField
|
@JvmField
|
||||||
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
|
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
|
||||||
private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
|
private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
|
||||||
private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage
|
private lateinit var fakeFavouriteContentLocalStorage: FavouriteContentLocalStorage
|
||||||
private lateinit var mockSessionExpirationListener: SessionExpirationListener
|
private lateinit var mockSessionExpirationListener: SessionExpirationListener
|
||||||
private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage
|
private lateinit var fakeUserDataLocalStorage: UserDataLocalStorage
|
||||||
private val isUserLoggedInUseCase by inject<IsUserLoggedInUseCase>()
|
private val isUserLoggedInUseCase by inject<IsUserLoggedInUseCase>()
|
||||||
private val loginUseCase by inject<LoginUseCase>()
|
private val loginUseCase by inject<LoginUseCase>()
|
||||||
private val logoutUseCase by inject<LogoutUseCase>()
|
private val logoutUseCase by inject<LogoutUseCase>()
|
||||||
|
|
@ -83,7 +91,7 @@ class AuthIntegrationTest : KoinTest {
|
||||||
|
|
||||||
@DisplayName("GIVEN no session WHEN user is logging in THEN they get session")
|
@DisplayName("GIVEN no session WHEN user is logging in THEN they get session")
|
||||||
@Test
|
@Test
|
||||||
fun login() = runTest {
|
fun loginSuccess() = runTest {
|
||||||
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true)
|
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true)
|
||||||
val expectedSession = ContentData.loginSuccessResponse
|
val expectedSession = ContentData.loginSuccessResponse
|
||||||
|
|
||||||
|
|
@ -108,20 +116,6 @@ class AuthIntegrationTest : KoinTest {
|
||||||
verifyZeroInteractions(mockSessionExpirationListener)
|
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")
|
@MethodSource("networkErrorArguments")
|
||||||
@ParameterizedTest(name = "GIVEN {0} network response WHEN login called THEN error is shown")
|
@ParameterizedTest(name = "GIVEN {0} network response WHEN login called THEN error is shown")
|
||||||
fun networkInputError(authScenario: AuthScenario) = runTest {
|
fun networkInputError(authScenario: AuthScenario) = runTest {
|
||||||
|
|
@ -136,7 +130,21 @@ class AuthIntegrationTest : KoinTest {
|
||||||
verifyZeroInteractions(mockSessionExpirationListener)
|
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
|
@Test
|
||||||
fun logout() = runTest {
|
fun logout() = runTest {
|
||||||
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true)
|
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true)
|
||||||
|
|
@ -145,10 +153,26 @@ class AuthIntegrationTest : KoinTest {
|
||||||
logoutUseCase.invoke()
|
logoutUseCase.invoke()
|
||||||
val actual = isUserLoggedInUseCase.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)
|
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<GetAllContentUseCase>().get().take(2).last()
|
||||||
|
logoutUseCase.invoke()
|
||||||
|
val valuesAfterLogout = get<GetAllContentUseCase>().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 {
|
companion object {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
||||||
|
|
@ -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<IsUserLoggedInUseCase>()
|
||||||
|
private val loginUseCase by inject<LoginUseCase>()
|
||||||
|
private val logoutUseCase by inject<LogoutUseCase>()
|
||||||
|
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 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package org.fnives.test.showcase.core.integration.fake
|
||||||
|
|
||||||
|
class CodeKataFavouriteContentLocalStorage {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package org.fnives.test.showcase.core.integration.fake
|
||||||
|
|
||||||
|
class CodeKataUserDataLocalStorage {
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue