From 8391f389aea471b23daf2797a269cc597d1f39e3 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 12 Jul 2022 09:48:30 +0300 Subject: [PATCH 1/6] Issue#94 Grammatical issues: Core Testing --- codekata/core.instructionset.md | 12 ++++++------ .../test/showcase/core/login/LoginUseCaseTest.kt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/codekata/core.instructionset.md b/codekata/core.instructionset.md index f47152d..b00d4c8 100644 --- a/codekata/core.instructionset.md +++ b/codekata/core.instructionset.md @@ -297,7 +297,7 @@ verifyNoInteractions(mockUserDataLocalStorage) Together: ```kotlin -@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() = runTest { val exception = RuntimeException() @@ -328,7 +328,7 @@ It has two methods: - getContents: that returns a Flow, which emits loading, error and content data - fetch: which suppose to clear cache and if the flow is observed then start loading -The content data come from a RemoteSource class. +The content data comes from a RemoteSource class. Additionally the Content is cached. So observing again should not yield loading. The inner workings of the class shouldn't matter, just the public apis, since that's what we want to test, always. @@ -353,7 +353,7 @@ fun setUp() { ### 1. `fetchingIsLazy` -As usual we are staring with the easiest test. We verify that the request is not called until the flow is not touched. +As usual we are starting with the easiest test. We verify that the request is not called until the flow is not touched. So just verify the request is not called yet: @@ -367,7 +367,7 @@ fun fetchingIsLazy() { ### 2. `happyFlow` -Next logical step is to verify the Happy flow. We setup the request to succeed and expect a Loading and Success state to be returned. +Next logical step is to verify the Happy flow. We set up the request to succeed and expect a Loading and Success state to be returned. ```kotlin val expected = listOf( @@ -592,7 +592,7 @@ Let's break down what changed with `UnconfinedTestDispatcher` Lastly so far we always assumed that we are getting the exact number of values take(4), take(2). However it's possible our flow may send out additional unexpected data. So we also need to test that this assumption is correct. -I think the best place to start from is our most complicated test `whenFetchingRequestIsCalledAgain` since this is the one most likely add additional unexpected values. +I think the best place to start from is our most complicated test `whenFetchingRequestIsCalledAgain` since this is the one most likely to add additional unexpected values. Luckily `async.isCompleted` is helpful here: We can check if the async actually finished, aka if it still suspended or complete. Alternatively when checking with values, we may use `async.getCompleted()` as well, since if a coroutine didn't finish properly it will throw an `IllegalStateException("This job has not completed yet")`. @@ -607,7 +607,7 @@ So our method looks similar to `whenFetchingRequestIsCalledAgain` except: - And requesting 5 elements instead of 4. - And cancel the async since we no longer need it -Note: if it confuses you why we need the additional `advanceUntilIdle` refer to the execution order descried above. The async got their 3rd and 4th values because we were using await. +Note: if it confuses you why we need the additional `advanceUntilIdle` refer to the execution order described above. The async got their 3rd and 4th values because we were using await. ```kotlin @DisplayName("GIVEN content response THEN error WHEN fetched THEN only 4 items are emitted") @Test diff --git a/core/src/test/java/org/fnives/test/showcase/core/login/LoginUseCaseTest.kt b/core/src/test/java/org/fnives/test/showcase/core/login/LoginUseCaseTest.kt index 718df68..8dfc089 100644 --- a/core/src/test/java/org/fnives/test/showcase/core/login/LoginUseCaseTest.kt +++ b/core/src/test/java/org/fnives/test/showcase/core/login/LoginUseCaseTest.kt @@ -89,7 +89,7 @@ internal class LoginUseCaseTest { verifyNoMoreInteractions(mockUserDataLocalStorage) } - @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() = runTest { val exception = RuntimeException() From 613cf22f1fa93a14e68b4840a65b1615102ff8aa Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 12 Jul 2022 09:54:21 +0300 Subject: [PATCH 2/6] Issue#94 Grammatical issues: Networking --- codekata/networking.instructionset.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/codekata/networking.instructionset.md b/codekata/networking.instructionset.md index 1da66ec..e293a25 100644 --- a/codekata/networking.instructionset.md +++ b/codekata/networking.instructionset.md @@ -14,7 +14,7 @@ Our System Under Test will be `org.fnives.test.showcase.network.auth.LoginRemote The login function sends out a retrofit request and parses a response for us, this is what we intend to test. -Let's setup our testClass: `org.fnives.test.showcase.network.auth.CodeKataLoginRemoteSourceTest` +Let's set up our testClass: `org.fnives.test.showcase.network.auth.CodeKataLoginRemoteSourceTest` ### Setup First since we are using Koin as Service Locator, we should extend [KoinTest](https://insert-koin.io/docs/reference/koin-test/testing/), thus giving us an easier way to access koin functions. @@ -25,7 +25,7 @@ class CodeKataLoginRemoteSourceTest : KoinTest { } ``` -Next we need to inject our System Under Test, setup koin and setup our [MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver). However we also need to tearDown our setup after every test. Let's take it step by step. +Next we need to inject our System Under Test, set up koin and set up our [MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver). However we also need to tearDown our setup after every test. Let's take it step by step. First we declare our required fields: ```kotlin @@ -47,7 +47,7 @@ fun tearDown() { } ``` -Now we need to setup our Koin: +Now we need to set up our Koin: ```kotlin @BeforeEach fun setUp() { @@ -110,7 +110,7 @@ fun tearDown() { Notice we are starting with `runBlocking` instead of `runTest`. That's because we do not have any concurrency, and also don't care about the Threads used by OkHttp, we just want to be sure to get the responses in sync. -First we need to setup mockwebserver to respond to our request and the expected value: +First we need to set up mockwebserver to respond to our request and the expected value: ```kotlin mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(readResourceFile("success_response_login.json"))) @@ -138,7 +138,7 @@ Now, running this test, you will see logs by OkHttpLoggingInterceptor and see wh So far we verified how to parse a response, but what about the validity of the request send out. This is what we will test next: -First we setup the mockwebserver just like before, however we no longer care about the returned value: +First we set up the mockwebserver just like before, however we no longer care about the returned value: ```kotlin mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(readResourceFile("success_response_login.json"))) ``` @@ -182,7 +182,7 @@ With this we can be sure our request contains exactly what we want it to contain Now we take a look at an expected error test: -First we setup our mockwebserver to return 400. This should mean our credentials were invalid. +First we set up our mockwebserver to return 400. This should mean our credentials were invalid. ```kotlin mockWebServer.enqueue(MockResponse().setResponseCode(400).setBody("")) @@ -278,7 +278,7 @@ The setup is already done since it's equivalent to our LoginRemoteSource tests e ### 1. `successRefreshResultsInRequestRetry` -First we need to setup our mockwebserver with the expected requests: +First we need to set up our mockwebserver with the expected requests: - 401 content request - success refresh token response - success content request with the new token @@ -312,7 +312,7 @@ val contentRequestAfterRefreshed = mockWebServer.takeRequest() ``` Next we need to verify -- the refresh request was properly setup +- the refresh request was properly set up - the new content request used the updated access token - no session expiration event was sent and token was saved @@ -335,7 +335,7 @@ verifyNoInteractions(mockNetworkSessionExpirationListener) Now we need to test what if the refresh request fails. -First setup for failure: +First, setup for failure: ```kotlin mockWebServer.enqueue(MockResponse().setResponseCode(401)) mockWebServer.enqueue(MockResponse().setResponseCode(400)) From 28e7638e7a9aa554d0d70f112aa975cbd3717f53 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 12 Jul 2022 09:56:04 +0300 Subject: [PATCH 3/6] Issue#94 Grammatical issues: ViewModel --- codekata/viewmodel.instructionset.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/codekata/viewmodel.instructionset.md b/codekata/viewmodel.instructionset.md index 87bf313..e7c7f3e 100644 --- a/codekata/viewmodel.instructionset.md +++ b/codekata/viewmodel.instructionset.md @@ -36,7 +36,7 @@ class CodeKataSplashViewModelTest { Note: you can use `@RegisterExtension` to register an extension as a field and make it easier to reference. -Next let's setup our System Under Test as usual: +Next let's set up our System Under Test as usual: ```kotlin private lateinit var mockIsUserLoggedInUseCase: IsUserLoggedInUseCase @@ -53,13 +53,13 @@ fun setUp() { ### 1. `loggedOutUserGoesToAuthentication` We want to test that if the user is not logged in then we are navigated to the Authentication screen. -So we need to setup the mock's response: +So we need to set up the mock's response: ```kotlin 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. +Next up we want to set up 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. @@ -133,7 +133,7 @@ The setup is already done because it's almost the same as mentioned in CodeKataS As always we start with the easiest test. This usually gives us motivation and helps us get ideas for the next tests. -First we setup the observers: +First, we set up the observers: ```kotlin val usernameTestObserver = sut.username.test() val passwordTestObserver = sut.password.test() @@ -221,7 +221,7 @@ navigateToHomeTestObserver.assertNoValue() Now let's test some actual logic: If we didn't give username and password to the ViewModel when login is clicked we should see loading, and empty string passed to the UseCase -Let's setup to login: +Let's set up to login: ```kotlin val loadingTestObserver = sut.loading.test() @@ -250,7 +250,7 @@ verifyNoMoreInteractions(mockLoginUseCase) Clicking the button once works as expected. But what if the user clicks the button multiple times before the request finishes? Let's make sure we only do actual actions once in such case. -We just setup the UseCase: +We just set up the UseCase: ```kotlin runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable())) } ``` @@ -271,7 +271,7 @@ verifyNoMoreInteractions(mockLoginUseCase) ### 6. `argumentsArePassedProperlyToLoginUseCase` Okay, now let's verify the UseCase receives the proper data. -We setup the UseCase response and update the username and password: +We set up the UseCase response and update the username and password: ```kotlin runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Error(Throwable())) @@ -325,7 +325,7 @@ navigateToHomeTestObserver.assertNoValue() ### 8. `invalidStatusResultsInErrorState` Time to test Errors. -First we setup our UseCase and the TestObservers: +First we set up our UseCase and the TestObservers: ```kotlin runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Success(LoginStatus.INVALID_CREDENTIALS)) @@ -390,7 +390,7 @@ Great, this is how we can reduce duplication in tests, without losing readabilit And finally let's test the happy flow as well. -We setup the observers and the UseCase: +We set up the observers and the UseCase: ```kotlin runBlocking { whenever(mockLoginUseCase.invoke(anyOrNull())).doReturn(Answer.Success(LoginStatus.SUCCESS)) From 13b27ac9d36d32010fd05a19a2c958a8c3a79c88 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 12 Jul 2022 09:57:54 +0300 Subject: [PATCH 4/6] Issue#94 Grammatical issues: Core.Again --- codekata/core.again.instructionset.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/codekata/core.again.instructionset.md b/codekata/core.again.instructionset.md index 9846562..44d10ae 100644 --- a/codekata/core.again.instructionset.md +++ b/codekata/core.again.instructionset.md @@ -170,7 +170,7 @@ fun withoutSessionTheUserIsNotLoggedIn() = runTest { 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: +First we set up 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 @@ -194,7 +194,7 @@ With this, looks like our Integration works correctly. Requests are called, prop ### 3. `localInputError` 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. +These two tests would be really similar, so let's do Parametrized tests. First we modify our method signature: ```kotlin @@ -311,7 +311,7 @@ verifyNoInteractions(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: +To do this, first we set up our MockServer and login the user: ```kotlin mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true) .setScenario(ContentScenario.Success(usingRefreshedToken = false), validateArguments = true) @@ -335,11 +335,11 @@ So what we want to verify, is that `valuesBeforeLogout` is a success, and the `v 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. +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 set up 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. +There is no point of going over other integration tests 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: From 559440884da9f8678a921d0676482c3edff126b4 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 12 Jul 2022 10:00:44 +0300 Subject: [PATCH 5/6] Issue#94 Grammatical issues: Robolectric --- codekata/robolectric.instructionset.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/codekata/robolectric.instructionset.md b/codekata/robolectric.instructionset.md index b3f390f..29f30d9 100644 --- a/codekata/robolectric.instructionset.md +++ b/codekata/robolectric.instructionset.md @@ -1,6 +1,6 @@ # 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 have 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. @@ -18,7 +18,7 @@ But we only test their interface functions. 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: +Let's set up or System Under Test as usual: ```kotlin private lateinit var sut: UserDataLocalStorage @@ -213,11 +213,11 @@ 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 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 switching out the koin-module creating the database is preferred. In other dependency injection / service locator frameworks this should also be possible. ### 1. `atTheStartOurDatabaseIsEmpty` -Our test is as simple as it gets. We get the observable and it's first element. Then we assert that it is an empty list. +Our test is as simple as it gets. We get the observable and its first element. Then we assert that it is an empty list. ```kotlin @Test @@ -275,7 +275,7 @@ 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: +So we set up our expectations and our observer: ```kotlin val expected = listOf(listOf(), listOf(ContentId("observe"))) val actual = async(coroutineContext) { sut.observeFavourites().take(2).toList() } @@ -300,7 +300,7 @@ Assert.assertEquals(expected, actual.getCompleted()) 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: +However for completeness sake: ```kotlin val expected = listOf(listOf(ContentId("a")), listOf()) sut.markAsFavourite(ContentId("a")) @@ -462,7 +462,7 @@ fun assertErrorIsNotShown() = apply { } ``` -With that our Robot is done, we can almost start Testing. We still need setup in our Test class. +With that our Robot is done, we can almost start Testing. We still need set up in our Test class. #### Test class setup @@ -516,7 +516,7 @@ 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. +> 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 its own thread pool, and we would like to have a way to await the responses. > 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. @@ -634,7 +634,7 @@ robot.assertErrorIsShown(R.string.username_is_invalid) ### 4. `invalidCredentialsGivenShowsProperErrorMessage` -Now we verify network errors. First let's setup the response: +Now we verify network errors. First let's set up the response: ```kotlin mockServerScenarioSetup.setScenario( AuthScenario.InvalidCredentials(username = "alma", password = "banan"), From d8a061565e429eb8605b8bcdc4698dd1843ea889 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 12 Jul 2022 10:07:34 +0300 Subject: [PATCH 6/6] Issue#94 Grammatical issues: SharedTest --- codekata/sharedtests.instructionset.md | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/codekata/sharedtests.instructionset.md b/codekata/sharedtests.instructionset.md index d74f0f0..2034ecc 100644 --- a/codekata/sharedtests.instructionset.md +++ b/codekata/sharedtests.instructionset.md @@ -8,7 +8,7 @@ In this testing instruction set you will learn how to write simple tests running - We will learn how to share classes between testing and AndroidTesting - Learn the differences between Robolectric and AndroidTests - Learn how to create End-To-End tests via Espresso Test Recorder -- Our tests classess will be really similar to Robolectric since we are using the same components +- Our tests classes will be really similar to Robolectric since we are using the same components - We will use RuleChains to order our Test Rules. ## Login UI Test @@ -20,7 +20,7 @@ Our classes will be `CodeKataAuthActivitySharedTest` and `CodeKataSharedRobotTes ### Setup #### Phone setup -First let's setup our phone. +First let's set up our phone. With testing on phone it's important that animations are disabled from the `Developer options`, namely: `Window animation scale` Animation Off `Transition animation scale` Animation Off @@ -34,7 +34,7 @@ Let's open `org.fnives.test.showcase.ui.login.codekata.CodeKataAuthActivityShare We can see it's identical as our original `org.fnives.test.showcase.ui.codekata.CodeKataAuthActivityInstrumentedTest`. So let's copy our existing code from the Robolectric test here. For that we can use the body of `org.fnives.test.showcase.ui.RobolectricAuthActivityInstrumentedTest`. -You immediately notice that there are no import issues. That's becacuse sharedTest package is added to the test sources. You may check out the `app/build.gradle` to see how that's done. +You immediately notice that there are no import issues. That's because sharedTest package is added to the test sources. You may check out the `app/build.gradle` to see how that's done. However we need to modify our robot: ```kotlin // Instead of this: @@ -62,10 +62,10 @@ java.lang.IllegalStateException: Cannot invoke setValue on a background thread ... ``` -So that's bring us to the first difference: *while Robolectric uses the same thread running the tests as running the Main thread, in Android Tests these threads are different.* +So that brings us to the first difference: *while Robolectric uses the same thread running the tests as running the Main thread, in Android Tests these threads are different.* -So the issue is with this line: `testDispatcher.advanceUntilIdleWithIdlingResources()`. Since we are in the InstrumentedTest's thread, all our coroutines will run there as well, which don't play well with LiveData. -One idea would be to use LiveData `ArchTaskExecutor.getInstance()` and ensure our LiveData don't care about the Thread they are set from, **But** then we would touch our Views from Non-Main Thread, which is still an issue. +So the issue is with this line: `testDispatcher.advanceUntilIdleWithIdlingResources()`. Since we are in the InstrumentedTest's thread, all our coroutines will run there as well, which doesn't play well with LiveData. +One idea would be to use LiveData `ArchTaskExecutor.getInstance()` and ensure our LiveData doesn't care about the Thread they are set from, **but** then we would touch our Views from Non-Main Thread, which is still an issue. **So Instead** What we need to do is run our coroutines on the actual mainThread. We have a handy `runOnUIAwaitOnCurrent` function for that, so let's use it in our `invalidCredentialsGivenShowsProperErrorMessage` test, wrap around our dispatcher call. The full function now will look like this: @@ -107,7 +107,7 @@ at org.fnives.test.showcase.ui.login.codekata.CodeKataAuthActivitySharedTest.tea ... ``` -Now that's a weird one, it points to our tearDown. So our test crashes in the `tearDown`, because the `mockServerScenarioSetup` is not initalized? +Now that's a weird one, it points to our tearDown. So our test crashes in the `tearDown`, because the `mockServerScenarioSetup` is not initialized? When you see similar crashes, that suggest you had an exception in your `setup` and it didn't finish, so the `tearDown` also fails, because not all the elements are initialized. If you select any other than the first that failed, and look for a root cause in the logs, you will see an issue along the lines of: @@ -128,7 +128,7 @@ So that's because something went wrong in our first test. I am describing these ``` Now, here is a new difference between Robolectric and AndroidTest. In Robolectric, before every test, the Application class is initialized, however in AndroidTests, the Application class is only initialized once. -This is great if you want to have End-to-End tests that follow each other, but since now we only want to tests some small subsection of the functionality, we have to restart Koin before every tests if it isn't yet started so our tests don't use the same instances. +This is great if you want to have End-to-End tests that follow each other, but since now we only want to test some small subsection of the functionality, we have to restart Koin before every tests if it isn't yet started, so our tests don't use the same instances. We will check if koin is initialized, if it isn't then we simply initialize it: ```kotlin ... @@ -150,8 +150,8 @@ With that now if you run the test class, all tests should succeed. ### 3. Animations One difference which may or may not happened on your phone is with loading indicators and animations. It happened on some of my devices and not on others (it can be different between Android API levels as well). -If it happens to yours, the tests won't succeed they just hang. This happens because animations can add continous work to the MainThread thus never letting it become idle. -The solution for this, to replace your Progress Bar or other infinetly animating element, with a simple view. +If it happens to yours, the tests won't succeed they just hang. This happens because animations can add continuous work to the MainThread thus never letting it become idle. +The solution for this, to replace your Progress Bar or other infinitely animating element, with a simple view. Some reference to this from stackoverflow [here](https://stackoverflow.com/questions/30469240/java-lang-runtimeexception-could-not-launch-intent-for-ui-with-indeterminate) and [here](https://stackoverflow.com/questions/35186902/testing-progress-bar-on-android-with-espresso). What I usually do is something like this in my Robot: @@ -198,9 +198,9 @@ override fun apply(base: Statement, description: Description): Statement = } } ``` -Pretty simple, we simple wrap the given Statement into our own, and in evaluation first we `init` the `Intents` and at the end we make sure it's `released`. +Pretty simple, we simply wrap the given Statement into our own, and in evaluation first we `init` the `Intents` and at the end we make sure it's `released`. -> Note: TestRule documentation contains a couple of other Base classes extending TestRule. Usually it's better to use one of them which matches describes your need, that's because it might take care of additional things you wouldn't expect otherwise. Here we could have used ExternalResource, but I wanted the Intents.init() in the try as well. +> Note: TestRule documentation contains a couple of other Base classes extending TestRule. Usually it's better to use one of them which matches your needs, that's because it might take care of additional things you wouldn't expect otherwise. Here we could have used ExternalResource, but I wanted the Intents.init() in the try as well. ##### Applying the Rule @@ -271,7 +271,7 @@ val testDispatcher get() = _testDispatcher ?: throw IllegalStateException("TestDispatcher is accessed before it is initialized!") ``` -We create the modifyable private field and make it accessable publicly to our tests. However if we access before a test is running, then we throw to let the user know. +We create the modifiable private field and make it accessible publicly to our tests. However if we access before a test is running, then we throw to let the user know. One addition is that it's probably better to also clear that dispatcher at the end, so: ```kotlin } finally { @@ -362,7 +362,7 @@ val ruleOrder: RuleChain = RuleChain.outerRule(intentRule) > Notice: we removed the Rule annotations from the others, and only have one Rule, the RuleChain. -The Rule chain starts our `intentRule` first, then the `mockServerAndKoinRule` and `mainDispatcherRule`, when cleaning up, first `mainDispatcherRule` will clean up, then the `mockServerAndKoinRule` and lastly then `intentRule`. That's because of the Statements, since one statement calls the other's evaluation. So the `IntentRule` received now the `mockServerAndKoinRule`'s statement, and the `mockServerAndKoinRule` received the `mainDispatcherRule`'s statement and they call evaluate on it. +The Rule chain starts our `intentRule` first, then the `mockServerAndKoinRule` and `mainDispatcherRule`, when cleaning up, first `mainDispatcherRule` will clean up, then `mockServerAndKoinRule` and lastly `intentRule`. That's because of the Statements, since one statement calls the other's evaluation. So the `IntentRule` received now the `mockServerAndKoinRule`'s statement, and the `mockServerAndKoinRule` received the `mainDispatcherRule`'s statement and they call evaluate on it. *TLDR: The rules are applied in the order in which they are added to the RuleChain* @@ -380,7 +380,7 @@ An Android developer writing with more detail, is [here](https://developer.andro So basically you can use this Test Recorder tool to create Espresso tests. You might be thinking, then why did we go through how to do these stuff manually?! Well, there are a couple of reasons: - the generated tests, at least for me, still use deprecated ActivityTestRule instead of ActivityScenario -- the generated tests, might still have issues in them, like syncronization with Okhttp and such. +- the generated tests, might still have issues in them, like synchronisation with Okhttp and such. - Some actions might be too specific, and you have to manually adjust the espresso test for it to work. All in all, it is a good tool to get started on your test, but you probably still need to do manual modifications on it. So personally I would suggest them for bigger tests, which would take too much time manually, and then do the adjustments while running the tests. @@ -432,7 +432,7 @@ And name your tests with an alphabetic order, like starting with numbers or some ### 6. Some notes on other differences you may face between Robolectric and AndroidTests #### 1. Hilt -Since currently only Koin is available in this repo, for updates follow this [issue](https://github.com/fknives/AndroidTest-ShowCase/issues/41), I thought to mentionen some issues with Hilt you may face: +Since currently only Koin is available in this repo, for updates follow this [issue](https://github.com/fknives/AndroidTest-ShowCase/issues/41), I thought to mentioned some issues with Hilt you may face: ##### Hilt requires a `HiltTestApplication` or something similar to test with. You can replace the test application by creating a `AndroidJUnitRunner` and return your Custom Application class. @@ -455,7 +455,7 @@ To resolve this fast, a possible way is like this: ```runBlocking { withContext(Dispatcher.IO) { mockwebserver.url("/") } }``` #### 3. Unnecessary initializations in Application class -An other issue can be that Crashlytics or similar services is enabled in your tests. This can be resolved by the same principle as the HiltTestApplication issue, aka custom `AndroidJunitRunner`. Your custom TestClass will initialize only what it needs to. +Another issue can be that Crashlytics or similar services is enabled in your tests. This can be resolved by the same principle as the HiltTestApplication issue, aka custom `AndroidJunitRunner`. Your custom TestClass will initialize only what it needs to. #### 4. Dialogs Dialogs cannot be tested properly via Robolectric without usage of Shadows, but they can be on Real Device. So what I usually do is setup a function which does one thing in one sourceset while does something else in another. You can see such example like `SpecificTestConfigurationsFactory`. To ease the usage I usually put a function in the sharedTest which uses the object `SpecificTestConfigurationsFactory`. @@ -469,7 +469,7 @@ The only real difference is that our tests are larger, maybe touching multiple s **Some personal thoughts** With all this described you should be able to start experimenting with testing. -Personally I would suggest to start with bug reports. When you get an simple bug ticket, write a test first which will fail if the bug is still present, then fix the bug. +Personally I would suggest to start with bug reports. When you get a simple bug ticket, write a test first which will fail if the bug is still present, then fix the bug. This both helps the project ensuring that behaviour will never happen again, and you are able to experiment with Testing. When you are more confident, you may start writing your features also together with small tests. These to me were started, when I had to work with timezones, dates or do similar calculations. These are great point to write tests, since it's a lot easier to see that your code works through examples then figuring out the whole thing at once. Instrumentation and UI Tests I would suggest only on features that are not likely to change a lot, since these are a bit more expensive to maintain, but these tests also ensure when you do changes you are not contradicting some previous requirements.