Proof read core instruction set
This commit is contained in:
parent
3c80744f6d
commit
9700a09c95
1 changed files with 29 additions and 29 deletions
|
|
@ -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`
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue