Proof read core instruction set

This commit is contained in:
Alex Gabor 2022-01-22 11:15:18 +02:00
parent 3c80744f6d
commit 9700a09c95

View file

@ -2,7 +2,7 @@
In this testing instruction set you will learn how to write simple tests using mockito. In this testing instruction set you will learn how to write simple tests using mockito.
Every test will be around one class and all of it's dependencies will be mocked out. Every test will be around one class and all of its dependencies will be mocked out.
Also suspend functions will be tested so you will see how to do that as well. Also suspend functions will be tested so you will see how to do that as well.
I would suggest to open this document in your browser, while working in Android Studio. I would suggest to open this document in your browser, while working in Android Studio.
@ -15,7 +15,7 @@ I would suggest to open this document in your browser, while working in Android
org.fnives.test.showcase.core.session.SessionExpirationAdapter org.fnives.test.showcase.core.session.SessionExpirationAdapter
``` ```
As you can see it's a simple adapter between an interface and it's received parameter. As you can see it's a simple adapter between an interface and its received parameter.
- Now navigate to the test class: - Now navigate to the test class:
@ -32,8 +32,8 @@ private lateinit var sut: SessionExpirationAdapter // System Under Testing
private lateinit var mockSessionExpirationListener: SessionExpirationListener private lateinit var mockSessionExpirationListener: SessionExpirationListener
``` ```
Now we need to initialize it, create a method names `setUp` and annotate it with `@BeforeEach` Now we need to initialize it. Create a method named `setUp` and annotate it with `@BeforeEach`
and initialize the `sut` variable, we will see that the adapter expects a constructor argument and initialize the `sut` variable. We will see that the adapter expects a constructor argument.
```kotlin ```kotlin
@BeforeEach // this means this method will be invoked before each test in this class @BeforeEach // this means this method will be invoked before each test in this class
@ -43,8 +43,8 @@ fun setUp() {
} }
``` ```
Great, now what is that mock? Simply put, it's a empty implementation of the interface. We can manipulate Great, now what is that mock? Simply put, it's an empty implementation of the interface. We can manipulate
that mock object to return what we want and verify it's method calls. that mock object to return what we want and verify its method calls.
### 2. First simple test ### 2. First simple test
@ -105,25 +105,25 @@ Now this is a bit more complicated, let's open our test file:
org.fnives.test.showcase.core.login.CodeKataSecondLoginUseCaseTest org.fnives.test.showcase.core.login.CodeKataSecondLoginUseCaseTest
``` ```
- declare the `sut` variable and it's dependencies, you should be familiar how to do this by now. - declare the `sut` variable and its dependencies, you should be familiar how to do this by now.
### 1. `emptyUserNameReturnsLoginStatusError` ### 1. `emptyUserNameReturnsLoginStatusError`
now let's write our first test: `emptyUserNameReturnsLoginStatusError` Now let's write our first test: `emptyUserNameReturnsLoginStatusError`
first we declare what kind of result we expect: First we declare what kind of result we expect:
```kotlin ```kotlin
val expected = Answer.Success(LoginStatus.INVALID_USERNAME) val expected = Answer.Success(LoginStatus.INVALID_USERNAME)
``` ```
next we do the actual invokation: Next we do the actual invocation:
```kotlin ```kotlin
val actual = sut.invoke(LoginCredentials("", "a")) val actual = sut.invoke(LoginCredentials("", "a"))
``` ```
lastly we add verification: Lastly we add verification:
```kotlin ```kotlin
Assertions.assertEquals(expected, actual) // assert the result is what we expected Assertions.assertEquals(expected, actual) // assert the result is what we expected
@ -228,7 +228,7 @@ Now we see how we can mock responses.
### 4. `validResponseResultsInSavingSessionAndSuccessReturned`, ### 4. `validResponseResultsInSavingSessionAndSuccessReturned`,
Now continue with `validResponseResultsInSavingSessionAndSuccessReturned`, You should have almost every tool to do this test: Now continue with `validResponseResultsInSavingSessionAndSuccessReturned`. You should have almost every tool to do this test:
- declare the expected value - declare the expected value
- do the mock response - do the mock response
- call the system under test - call the system under test
@ -255,15 +255,15 @@ fun validResponseResultsInSavingSessionAndSuccessReturned() = runTest {
### 5. `invalidResponseResultsInErrorReturned` ### 5. `invalidResponseResultsInErrorReturned`
this is really similar to our previous test, however now somehow we have to mock throwing an exception This is really similar to our previous test, however now somehow we have to mock throwing an exception
to do this let's create an exception: To do this let's create an exception:
```kotlin ```kotlin
val exception = RuntimeException() val exception = RuntimeException()
``` ```
declare our expected value: Declare our expected value:
```kotlin ```kotlin
val expected = Answer.Error<LoginStatus>(UnexpectedException(exception)) val expected = Answer.Error<LoginStatus>(UnexpectedException(exception))
@ -275,13 +275,13 @@ Do the mocking:
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b"))).doThrow(exception) whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b"))).doThrow(exception)
``` ```
invocation: Invocation:
```kotlin ```kotlin
val actual = sut.invoke(LoginCredentials("a", "b")) val actual = sut.invoke(LoginCredentials("a", "b"))
``` ```
verification: Verification:
```kotlin ```kotlin
Assertions.assertEquals(expected, actual) Assertions.assertEquals(expected, actual)
@ -292,7 +292,7 @@ verifyZeroInteractions(mockUserDataLocalStorage)
- and the pattern of GIVEN-WHEN-THEN description. - and the pattern of GIVEN-WHEN-THEN description.
``` ```
together: Together:
```kotlin ```kotlin
@DisplayName("GIVEN error resposne WHEN trying to login THEN session is not touched and error is returned") @DisplayName("GIVEN error resposne WHEN trying to login THEN session is not touched and error is returned")
@ -323,7 +323,7 @@ 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. The inner workings of the class shouldn't matter, just the public apis, since that's what we want to test.
For setup we declare the system under test and it's mock argument. For setup we declare the system under test and its mock argument.
```kotlin ```kotlin
private lateinit var sut: ContentRepository private lateinit var sut: ContentRepository
@ -368,7 +368,7 @@ Next the action:
val actual = sut.contents.take(2).toList() val actual = sut.contents.take(2).toList()
``` ```
Now just the verifications Now just the verifications:
```kotlin ```kotlin
Assertions.assertEquals(expected, actual) Assertions.assertEquals(expected, actual)
@ -398,7 +398,7 @@ Assertions.assertEquals(expected, actual)
### 4. `verifyCaching` ### 4. `verifyCaching`
Still sticking to just that function, we should verify it's caching behaviour, aka if a data was loaded once the next time we observe the flow that data is returned: Still sticking to just that function, we should verify its caching behaviour, aka if a data was loaded once the next time we observe the flow that data is returned:
The setup is similar to the happy flow, but take a look at the last line closely The setup is similar to the happy flow, but take a look at the last line closely
```kotlin ```kotlin
@ -413,7 +413,7 @@ The action will only take one element which we expect to be the cache
val actual = sut.contents.take(1).toList() val actual = sut.contents.take(1).toList()
``` ```
In the verification state, we will also make sure the request indead was called only once: In the verification state, we will also make sure the request indeed was called only once:
```kotlin ```kotlin
verify(mockContentRemoteSource, times(1)).get() verify(mockContentRemoteSource, times(1)).get()
Assertions.assertEquals(expected, actual) Assertions.assertEquals(expected, actual)
@ -421,7 +421,7 @@ Assertions.assertEquals(expected, actual)
### 5. `loadingIsShownBeforeTheRequestIsReturned` ### 5. `loadingIsShownBeforeTheRequestIsReturned`
So far we just expected the first element is "loading", but it could easily happen that the flow set up in such a way that the loading is not emitted So far we just expected the first element is "loading", but it could easily happen that the flow is set up in such a way that the loading is not emitted
before the request already finished. before the request already finished.
This can be an easy mistake with such flows, but would be really bad UX, so let's see how we can verify something like that: This can be an easy mistake with such flows, but would be really bad UX, so let's see how we can verify something like that:
@ -433,7 +433,7 @@ Generally we could still use mockito mocks OR we could create our own Mock.
#### Creating our own mock. #### Creating our own mock.
We can simply implement the interface of ContentRemoteSource. Have a it's method suspend until a signal. We can simply implement the interface of ContentRemoteSource. Have it's method suspend until a signal.
Something along the way of: Something along the way of:
@ -460,13 +460,13 @@ In this case we should recreate our sut in the test and feed it our own remote s
To mock such behaviour with mockito with our current tool set is not as straight forward as creating our own. To mock such behaviour with mockito with our current tool set is not as straight forward as creating our own.
That's because how we used mockito so far it is not aware of the nature of suspend functions, like our code is in the custom mock. That's because how we used mockito so far it is not aware of the nature of suspend functions, like our code is in the custom mock.
However mockito give us the arguments passed into the function. However mockito gives us the arguments passed into the function.
And since we know the Continuation object is passed as a last argument in suspend functions we can take advantage of that. And since we know the Continuation object is passed as a last argument in suspend functions we can take advantage of that.
This then can be abstracted away and used wherever without needing to create Custom Mocks for every such case. This then can be abstracted away and used wherever without needing to create Custom Mocks for every such case.
To get arguments when creating a response for the mock you need to use thenAnswer { } and this lambda will receive InvocationOnMock containing the arguments. To get arguments when creating a response for the mock you need to use thenAnswer { } and this lambda will receive InvocationOnMock containing the arguments.
Luckily this has already be done in "org.mockito.kotlin" and it's called `doSuspendableAnswer` Luckily this has already been done in "org.mockito.kotlin" and it's called `doSuspendableAnswer`
The point here is that we can get arguments while mocking with mockito, and we are able to extend it in a way that helps us in common patterns. The point here is that we can get arguments while mocking with mockito, and we are able to extend it in a way that helps us in common patterns.
@ -502,7 +502,7 @@ suspendedRequest.complete(Unit)
We still didn't even touch the fetch method so let's test the behaviour next: We still didn't even touch the fetch method so let's test the behaviour next:
We want to get the first result triggered by the subscription to the flow, and the again another loading and result after a call to `fetch`, so the setup would be: We want to get the first result triggered by the subscription to the flow, and then again another loading and result after a call to `fetch`, so the setup would be:
```kotlin ```kotlin
val exception = RuntimeException() val exception = RuntimeException()
val expected = listOf( val expected = listOf(
@ -531,7 +531,7 @@ Assertions.assertEquals(expected, actual.await())
``` ```
However this test will hang. This is because `runTest` uses by default `StandardTestDispatcher` which doesn't enter child coroutines immediately and the async block will only be executed after the call to fetch. However this test will hang. This is because `runTest` uses by default `StandardTestDispatcher` which doesn't enter child coroutines immediately and the async block will only be executed after the call to fetch.
This is a good thing because it gives us more control over the order of execution and as a result our test are not shaky. This is a good thing because it gives us more control over the order of execution and as a result our tests are not shaky.
To make sure that `fetch` is called only when `take` suspends, we can call `advanceUntilIdle` which will give the opportunity of the async block to execute. To make sure that `fetch` is called only when `take` suspends, we can call `advanceUntilIdle` which will give the opportunity of the async block to execute.
So our test becomes: So our test becomes:
```kotlin ```kotlin
@ -553,7 +553,7 @@ fun whenFetchingRequestIsCalledAgain() = runTest(UnconfinedTestDispatcher()) {
} }
``` ```
Now we can test even complicated interactions between methods and classes with TestCoroutineDispatcher. Now we can test even complicated interactions between methods and classes with test dispatchers.
### 7. `noAdditionalItemsEmitted` ### 7. `noAdditionalItemsEmitted`