diff --git a/codekata/core.again.instructionset.md b/codekata/core.again.instructionset.md index 49c410d..9846562 100644 --- a/codekata/core.again.instructionset.md +++ b/codekata/core.again.instructionset.md @@ -130,11 +130,11 @@ We are still missing `mockServerScenarioSetupExtensions` this will be our TestEx `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 can be used to depend on a specific test module "textFixtures". +That's because of [java-test-fixtures](https://docs.gradle.org/current/userguide/java_testing.html#sec:java_test_fixtures) plugin. It can be used to depend on a specific test module "testFixtures". 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. -> Test Fixtrues creates a new sourceset between the production code and the test code. test depends on testFixtures and testFixtures depends on source. So test sees everything in testFixtures and other modules can also use testFixtures. This way we can share extensions or other helper classes. +> Test Fixtures create a new sourceset between the production code and the test code. test depends on testFixtures and testFixtures depends on source. So test sees everything in testFixtures and other modules can also use testFixtures. This way we can share extensions or other helper classes. > An alternative to use test code between modules instead of TestFixtures is to use a separate module, like the :mockserver defined in the project. So let's add this extension: @@ -161,14 +161,14 @@ fun withoutSessionTheUserIsNotLoggedIn() = runTest { fakeUserDataLocalStorage.session = null val actual = isUserLoggedInUseCase.invoke() - Assertions.assertFalse(actual, "User is expected to be not logged in") + Assertions.assertFalse(actual, "User is expected not to be logged in") verifyNoInteractions(mockSessionExpirationListener) } ``` ### 2. `loginSuccess` -Let's test that given good credentials and success response, our user can login in. +Let's test that given good credentials and success response, our user can log in. First we setup our mock server and the expected session: ```kotlin @@ -193,7 +193,7 @@ verifyNoInteractions(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. +We have two 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: @@ -212,7 +212,7 @@ 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.assertFalse(actual, "User is expected not to be logged in") Assertions.assertEquals(null, fakeUserDataLocalStorage.session) verifyNoInteractions(mockSessionExpirationListener) ``` diff --git a/codekata/core.instructionset.md b/codekata/core.instructionset.md index 4cb94ec..f47152d 100644 --- a/codekata/core.instructionset.md +++ b/codekata/core.instructionset.md @@ -452,13 +452,12 @@ We can simply implement the interface of ContentRemoteSource. Have it's method s Something along the way of: ```kotlin -class SuspendingContentRemoteSource { +class SuspendingContentRemoteSource : ContentRemoteSource { private var completableDeferred = CompletableDeferred() @Throws(NetworkException::class, ParsingException::class) - suspend fun get(): List { - completableDeferred = CompletableDeferred() + override suspend fun get(): List { completableDeferred.await() return emptyList() } diff --git a/codekata/robolectric.instructionset.md b/codekata/robolectric.instructionset.md index fa97c04..b3f390f 100644 --- a/codekata/robolectric.instructionset.md +++ b/codekata/robolectric.instructionset.md @@ -35,11 +35,11 @@ And if we run our test class we already get an exception: at org.fnives.test.showcase.storage.SharedPreferencesManagerImpl$Companion.create(SharedPreferencesManagerImpl.kt:65) So we need to mock the creation of `SharedPreferences`, then the `SharedPreferences` as well. -Since our classes main purpose is to handle `SharedPreferences`, that doesn't really make sense. +Since our class's main purpose is to handle `SharedPreferences`, that doesn't really make sense. Well, I would rather not do that. So then we need to run our tests on a Real Device or Emulator during development. Well we could do that, but it just takes that much more time. -We would also need to to integrate a Testing Farm, or run Emulators in docker with our CI. +We would also need to integrate a Testing Farm, or run Emulators in docker 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. @@ -60,7 +60,7 @@ class CodeKataUserDataLocalStorageTest: KoinTest { } ``` -Okay, now we just need to get a context. With Robolectric we can get our application class the following way: +Okay, now we just need to get a context as follows: ```kotlin val application = ApplicationProvider.getApplicationContext() @@ -101,7 +101,7 @@ Assert.assertEquals(null, actual) ### 3. Fake So if you are doing these instructions in order, you may remember that in our core integration tests, namely `org.fnives.test.showcase.core.integration.CodeKataAuthIntegrationTest` we actually had a Fake implementation of this class. -But we never verified that the Fake behaves exactly as will the real thing, so let's do that. +But we never verified that the Fake behaves exactly as the real thing, so let's do that. Sadly we can't depend on the `org.fnives.test.showcase.core.integration.fake.CodeKataUserDataLocalStorage` since it's in a test module. However with usage of testFixtures we are able to share test classes as we had previously shared an Extension. Take a look at `core/src/testFixtures/java`, in package `org.fnives.test.showcase.core.integration.fake` We have a `FakeUserDataLocalStorage`. We can use that since it's in the testFixture. @@ -116,7 +116,7 @@ To do that we will parametrize our test. Note, it will be different than previou Let's modify our annotation and Test Class constructor: ```kotlin @RunWith(ParameterizedRobolectricTestRunner::class) -class CodeKataUserDataLocalStorageTest(val userDataLocalStorageFactory: () -> UserDataLocalStorage) : TestKoin { +class CodeKataUserDataLocalStorageTest(private val userDataLocalStorageFactory: () -> UserDataLocalStorage) : KoinTest { //... } ``` @@ -188,7 +188,7 @@ class CodeKataFavouriteContentLocalStorage: KoinTest Since Room has their own executors, that could make our tests flaky, since it 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. -``` +```kotlin private val sut by inject() private lateinit var testDispatcher: TestDispatcher @@ -213,7 +213,7 @@ The line `TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatc > Above min API 24 > DatabaseInitialization could be overwritten in Test module, by declaring the same class in the same package with the same methods. This is an easy way to switch out an implementation. -> That might not look the cleanest, so an the presented way of switch out the koin-module creating the database is preferred. In other dependency injection / service locator frameworks this should also be possible. +> That might not look the cleanest, so the presented way of switch out the koin-module creating the database is preferred. In other dependency injection / service locator frameworks this should also be possible. ### 1. `atTheStartOurDatabaseIsEmpty` @@ -288,7 +288,7 @@ 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. +And let's assert that indeed we only get these two updates and nothing more happens. 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()) @@ -355,10 +355,10 @@ If you want to check it out, `FavouriteContentLocalStorageImplInstrumentedTest` We can do much more with Robolectric than just test our Database or SharedPreferences. We can write UI Tests as well. It is still not as good as Running tests on a Real Device. But depending on your need it might still be helpful. -> Note we get to the section where I am the least comfortable with, I don't think I have written enough UI Tests yet, so from now on take evrything with a big grain of salt. Feel free to modify your approach to your need. You may also correct me via issues on GitHub, would be a great pleasure to learn for me. +> Note we get to the section where I am the least comfortable with, I don't think I have written enough UI Tests yet, so from now on take everything with a big grain of salt. Feel free to modify your approach to your need. You may also correct me via issues on GitHub, would be a great pleasure to learn for me. -We can write UI tests that have mocked out UseCases and Business Logic, but I prefer to do a full screen Integration Tests, cause I think my UI changes enough as it is, wouldn't want to maintain one extra testing layer. -So this will be showcased here. But you should be able to write pure UI tests, if you can follow along this section as well if you choose to do so +We can write UI tests that have mocked out UseCases and Business Logic, but I prefer to do a full screen Integration Tests, cause I think my UI changes enough already and I wouldn't want to maintain one extra testing layer. +So this will be showcased here. But you should be able to write pure UI tests, if you can follow along this section as well if you choose not to do so. ### Setup @@ -368,23 +368,21 @@ First of all we will use [Espresso](https://developer.android.com/training/testi We need quite a bunch of setup, but first let's start with our Robot. #### Robot Pattern -Robot Pattern presented by Jake Wharton here: https://academy.realm.io/posts/kau-jake-wharton-testing-robots/ and as described Kotlin specific here: https://medium.com/android-bits/espresso-robot-pattern-in-kotlin-fc820ce250f7 - -There is also a Kotlin specific article [here](https://medium.com/android-bits/espresso-robot-pattern-in-kotlin-fc820ce250f7). +The Robot Pattern is presented by Jake Wharton [here](https://jakewharton.com/testing-robots/) and there is also a Kotlin specific article describing the same pattern [here](https://medium.com/android-bits/espresso-robot-pattern-in-kotlin-fc820ce250f7). The idea is to separate the logic of finding your views from the logic of the test. So basically if for example a View Id changes, it doesn't make our behaviour change too, so in this case only our Robot will change, while the Test Class stays the same. -For now I will keep the synthetic sugar to the minimum, and just declare my actions and verifications there. Feel free to have as much customization there as you think is necessary to make your tests clearer. +For now I will keep the syntactic sugar to a minimum, and just declare my actions and verifications there. Feel free to have as much customization there as you think is necessary to make your tests clearer. Let's open our robot: `org.fnives.test.showcase.ui.codekata.CodeKataLoginRobot` Here is a list of actions we want to do: - we want to be able to type in the username - we want to be able to type in the password -- we want to be able the username or password is indeed shows on the UI +- we want to be able to verify that the username or password is indeed shown on the UI - we want to be able to click on signin -- we want to be able verify if we are loading or not +- we want to be able to verify if we are loading or not - we want to verify if an error is shown or not - we want to check if we navigated to Main or not @@ -427,8 +425,8 @@ fun assertNotLoading() = apply { } ``` -Here we took advantage of Espresso. It helps us by being able to perform action such as click, find Views, such as by ID, and assert View States such as withText. -To know what Espresso matchers, assertions are there you just have to use them. It's also easy to extend so if one of your views doesn't have that option, then you can create your own matcher. +Here we took advantage of Espresso. It helps us by being able to perform actions, such as clicks, find Views, such as by ID, and assert View States, such as withText. +To know what Espresso matches, assertions are there you just have to use them. It's also easy to extend so if one of your views doesn't have that option, then you can create your own matcher. ##### Next up, we need to verify if we navigated: @@ -442,7 +440,7 @@ fun assertNotNavigatedToHome() = apply { } ``` -Here we use Espresso's intents, with this we can verify if an Intent was sent out we can also Intercept it to send a result back. +Here we use Espresso's intents, with this we can verify if an Intent was sent out. We can also Intercept it to send a result back. ##### Lastly let's verify Errors For Snackbar we still gonna use Espresso, but we have a helper class for that because we may reuse it in other places. @@ -519,8 +517,8 @@ fun tearDown() { ``` > Idling Resources comes from Espresso. The idea is that anytime we want to interact with the UI via Espresso, it will await any Idling Resource beforehand. This is handy, since our Network component, (OkHttp) uses it's own thread pool, and we would like to have a way to await the responses. -> Disposable is just a synthetic-sugar way to remove the OkHttpIdling resource from Espresso when we no longer need it. -> Idling Resources also makes it easy for us, to coordinate coroutines with our network responses, since we can await the IdlingResource and advance the Coroutines afterwards. +> Disposable is just a syntactic-sugar to remove the OkHttpIdling resource from Espresso when we no longer need it. +> Idling Resources also make it easy for us, to coordinate coroutines with our network responses, since we can await the IdlingResource and advance the Coroutines afterwards. ##### Coroutine Test Setup We use a TestDispatcher and initialize our database with it as well. @@ -547,7 +545,7 @@ fun tearDown() { ##### Finally we initialize our UI -We create our Robot. And we take advantage or `ActivityScenario` to handle the lifecycle of the Activity. +We create our Robot and we take advantage of `ActivityScenario` to handle the lifecycle of the Activity. ```kotlin @Before fun setup() { @@ -577,7 +575,7 @@ First we mock our request: ```kotlin mockServerScenarioSetup.setScenario( AuthScenario.Success(password = "alma", username = "banan"), - validateArguments = true) + validateArguments = true ) ``` @@ -639,8 +637,8 @@ robot.assertErrorIsShown(R.string.username_is_invalid) Now we verify network errors. First let's setup the response: ```kotlin mockServerScenarioSetup.setScenario( - AuthScenario.InvalidCredentials(username = "alma", password = "banan"), - validateArguments = true + AuthScenario.InvalidCredentials(username = "alma", password = "banan"), + validateArguments = true ) ``` @@ -692,7 +690,7 @@ robot.assertErrorIsShown(R.string.something_went_wrong) ### Shadows We don't have an example to work through Shadows for now, since it might not be necessary for everyday applications. An example will still be added [here](https://github.com/fknives/AndroidTest-ShowCase/pull/57) at some point. -Since Robolectric is a faking of the Android Framework, there are limitations to it's usage. For example if our Application interacts with the device's AudioManager. Robolectric won't hook into our actual operating system and listen to sounce changes or AudioFocus, but use a mocked/Faked class instead. +Since Robolectric is a faking of the Android Framework, there are limitations to it's usage. For example if our Application interacts with the device's AudioManager, Robolectric won't hook into our actual operating system and listen to sound changes or AudioFocus, but use a mocked/Faked class instead. If we need to emulate some kind of interaction with that component, that's when Shadows come in. Example: [ShadowAudioManager](http://robolectric.org/javadoc/4.1/org/robolectric/shadows/ShadowAudioManager.html). What happens here is while testing, instead of creating an actual AudioManager, this ShadowAudioManager would be created. We can modify it's implementation or use it like a Fake implementation by feeding it values to be returned. That way we can still test our application against the Android API while still having full control over the responses. We can create custom shadows or overwrite existing ones to fit our use cases better. diff --git a/codekata/viewmodel.instructionset.md b/codekata/viewmodel.instructionset.md index 1a4285c..87bf313 100644 --- a/codekata/viewmodel.instructionset.md +++ b/codekata/viewmodel.instructionset.md @@ -34,9 +34,9 @@ To add this to our TestClass we need to do the following: class CodeKataSplashViewModelTest { ``` -Note you can use `@RegisterExtension` to register an extension as a field and make it easier to reference. +Note: you can use `@RegisterExtension` to register an extension as a field and make it easier to reference. -Next let's setup or System Under Test as usual: +Next let's setup our System Under Test as usual: ```kotlin private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase @@ -60,7 +60,7 @@ whenever(mockIsUserLoggedInUseCase.invoke()).doReturn(false) ``` Next up we want to setup our TestObserver for LiveData. This enables us to verify the values sent into a LiveData. -If a livedata is not observed, its value may not be updated (like a livedata that maps) so it's important to have a proper TestObserver set. +If a LiveData is not observed, its value may not be updated (like a LiveData that maps) so it's important to have a proper TestObserver set. ```kotin @@ -108,7 +108,7 @@ val navigateToTestObserver = sut.navigateTo.test() testScheduler.advanceTimeBy(100) // we wait only 100ms not 500ms ``` -And the as verification we just check that no values were submitted. +And as verification we just check that no values were submitted. ```kotlin navigateToTestObserver.assertNoValue() // this is the way to test that no value has been sent out @@ -177,6 +177,12 @@ sut.onPasswordChanged("a") sut.onPasswordChanged("al") ``` +Advance the test scheduler before proceeding with the verifications. + +```kotlin +testScheduler.advanceUntilIdle() +``` + And at the end we verify the passwordTestObserver was updated and the others weren't: ```kotlin @@ -343,7 +349,7 @@ navigateToHomeTestObserver.assertNoValue() ``` Probably you are already getting bored of writing almost the same tests, and we need 2 more tests just like this only for different Error types. -So let's not write the same test again, but parametrize this one. +So let's not write the same test again, but parametrize this one instead. First we need to annotate our test, signal that it should be parametrized: ```kotlin @@ -355,7 +361,7 @@ fun invalidStatusResultsInErrorState( ) ``` -Define the parameters for our tests, the field should be static and notice the field name: +Define the parameters for our tests, the method should be static and notice its name: ```kotlin companion object { @@ -408,11 +414,11 @@ navigateToHomeTestObserver.assertValueHistory(Event(Unit)) ``` ## Conclusion -That concludes or ViewModel tests. +That concludes our ViewModel tests. As you can see it's not too different from the previous tests, we just needed to add a couple of additional setup and helper classes. With this we are able to: - Test ViewModels - Test LiveData - Use TestScheduler for ViewModels -- How to use Test Extensions -- How to parametrize tests to reduce duplication +- Use Test Extensions +- Parametrize tests to reduce duplication