Skip to content
This repository was archived by the owner on Nov 14, 2024. It is now read-only.

Commit 6604f4d

Browse files
testing raise for video
1 parent 7b90568 commit 6604f4d

File tree

1 file changed

+39
-43
lines changed

1 file changed

+39
-43
lines changed

_posts/2024-08-23-testing-raise.md

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ dependencies {
2323
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
2424
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0")
2525
testImplementation("org.assertj:assertj-core:3.26.3")
26+
testImplementation("in.rcard:assertj-arrow-core:1.1.0")
2627
testImplementation("io.kotest:kotest-runner-junit5:5.9.0")
2728
testImplementation("io.kotest.extensions:kotest-assertions-arrow:1.4.0")
2829
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
30+
testImplementation("io.mockk:mockk:1.13.12")
2931
}
3032
```
3133

@@ -41,6 +43,9 @@ data class CreatePortfolio(
4143
val amount: Money,
4244
)
4345

46+
@JvmInline
47+
value class Money(val amount: Double)
48+
4449
@JvmInline
4550
value class UserId(val id: String)
4651

@@ -61,6 +66,8 @@ We can start implementing the use case now that we have set up. Since we are dil
6166
First, we want to test the happy path, meaning the use case creates a new portfolio for the user. We need to implement our use case interface with a concrete class, which we usually call a service:
6267

6368
```kotlin
69+
import arrow.core.raise.Raise
70+
6471
fun createPortfolioUseCase(): CreatePortfolioUseCase =
6572
object : CreatePortfolioUseCase {
6673
override fun Raise<DomainError>.createPortfolio(model: CreatePortfolio): PortfolioId = TODO()
@@ -72,8 +79,9 @@ As you might have noticed, the `createPortfolioUseCase` method is nothing more t
7279
We'll use different testing frameworks. Let's begin with a setup that should be familiar to developers addicted to Kotlin and Spring: **JUnit 5 as the test runtime and AssertJ for assertions**.
7380

7481
```kotlin
82+
import kotlin.test.Test
83+
7584
internal class CreatePortfolioUseCaseJUnit5Test {
76-
7785
private val underTest = createPortfolioUseCase()
7886

7987
@Test
@@ -89,6 +97,9 @@ Now, we have to test that, given some inputs, the function will return the expec
8997
However, the below implementation will not even compile:
9098

9199
```kotlin
100+
import arrow.core.Either
101+
import arrow.core.raise.either
102+
92103
@Test
93104
internal fun `given a userId and an initial amount, when executed, then it create the portfolio`() {
94105
val actualResult: PortfolioId =
@@ -152,7 +163,8 @@ Now, we can implement the function `createPortfolio` to make the test pass. Let'
152163
```kotlin
153164
fun createPortfolioUseCase(): CreatePortfolioUseCase =
154165
object : CreatePortfolioUseCase {
155-
override fun Raise<DomainError>.createPortfolio(model: CreatePortfolio): PortfolioId = PortfolioId("1")
166+
override fun Raise<DomainError>.createPortfolio(model: CreatePortfolio): PortfolioId =
167+
PortfolioId("1")
156168
}
157169
```
158170

@@ -161,13 +173,15 @@ If we run our test, it should be green.
161173
Instead of transforming the `Raise<E>.() -> A` function in a `() -> Either<E, A>` function, **we can use the `fold` function provided by the Arrow library**:
162174

163175
```kotlin
176+
import org.assertj.core.api.Assertions
177+
164178
@Test
165179
internal fun `given a userId and an initial amount, when executed, then it create the portfolio (using fold)`() {
166180
fold(
167181
block = {
168182
with(underTest) {
169183
createPortfolio(CreatePortfolio(UserId("bob"), Money(1000.0)))
170-
},
184+
}
171185
},
172186
recover = { Assertions.fail("The use case should not fail") },
173187
transform = { Assertions.assertThat(it).isEqualTo(PortfolioId("1")) },
@@ -178,6 +192,9 @@ internal fun `given a userId and an initial amount, when executed, then it creat
178192
However, the above code is cumbersome and less readable than the previous one. Moreover, we must apply a `fold` function whenever we want to test a function declared in a `Raise<E>` context. Fortunately, the `assertj-arrow-core` does it for us, defining some handful of assertions that use the `fold` function under the hood:
179193

180194
```kotlin
195+
import `in`.rcard.assertj.arrowcore.RaiseAssert
196+
197+
181198
@Test
182199
internal fun `given a userId and an initial amount, when executed, then it create the portfolio (using RaiseAssert)`() {
183200
RaiseAssert
@@ -198,11 +215,15 @@ The test is less readable than the one with the `either` builder because the lib
198215

199216
We have used JUnit 5 until now. However, we can switch to Kotest. **Kotest is a robust testing framework for Kotlin**, which is very close to ScalaTest for the Scala language. Kotest also has a set of tailored assertions for some of the available types in the Arrow library (see the [documentation](https://kotest.io/docs/assertions/arrow.html) for further details).
200217

201-
So, let's translate the above tests in Kotest notation.
218+
If you're writing code in IntelliJ IDEA, it might be worth installing the Kotest plugin to be able to run the tests directly in the IDE.
219+
220+
So, let's translate the above tests in [Kotest](https://plugins.jetbrains.com/plugin/14080-kotest-plugin-intellij) notation.
202221

203222
```kotlin
204-
internal class CreatePortfolioUseCaseKotestTest : ShouldSpec({
223+
import io.kotest.core.spec.style.ShouldSpec
224+
import io.kotest.assertions.arrow.core.*
205225

226+
internal class CreatePortfolioUseCaseKotestTest : ShouldSpec({
206227
val underTest = createPortfolioUseCase()
207228

208229
context("The create portfolio use case") {
@@ -250,22 +271,22 @@ Let's say we only have one portfolio per user. So, we need to check if the user
250271
First, we now have a way for our use case to fail: A portfolio for a user may already exist. So, we need to add a new error:
251272

252273
```kotlin
253-
sealed interface DomainError {
254-
data class PortfolioAlreadyExists(val userId: UserId) : DomainError
255-
}
274+
data class PortfolioAlreadyExists(val userId: UserId) : DomainError
256275
```
257276

258277
Then, we can define the new port interface. Given a `userId`, we can count the user's portfolios.
259278

260279
```kotlin
261280
interface CountUserPortfoliosPort {
262-
fun Raise<DomainError>.countByUserId(userId: UserId): Int
281+
fun countByUserId(userId: UserId): Int
263282
}
264283
```
265284

266285
Finally, let's wire all the things together, starting using our port into the use case:
267286

268287
```kotlin
288+
import arrow.core.raise.*
289+
269290
fun createPortfolioUseCase(countUserPortfolios: CountUserPortfoliosPort): CreatePortfolioUseCase =
270291
object : CreatePortfolioUseCase {
271292
override fun Raise<DomainError>.createPortfolio(model: CreatePortfolio): PortfolioId {
@@ -288,7 +309,7 @@ In our case, we need to implement the port for our test. For example, we want to
288309
```kotlin
289310
private val fakeCountUserPortfolios: CountUserPortfoliosPort =
290311
object : CountUserPortfoliosPort {
291-
override fun Raise<DomainError>.countByUserId(userId: UserId): Int =
312+
override fun countByUserId(userId: UserId): Int =
292313
if (userId == UserId("bob")) 0 else 1
293314
}
294315
```
@@ -324,52 +345,25 @@ There are a lot of libraries that can help us to create mocks. The most famous i
324345
**Mocking a dependency is a three-step process**. First, you need to retrieve from the library an empty mock of the dependency:
325346

326347
```kotlin
348+
import io.mockk.mockk
349+
327350
val countUserPortfoliosMock: CountUserPortfoliosPort = mockk()
328351
```
329352

330353
The `mockk()` factory function provides a proxy to the port we can use to instrument our needs. The second step is the instrumentation of the mock indeed, and we should instrument the `countUserPortfoliosMock` in the following way:
331354

332355
```kotlin
333-
every {
334-
with(countUserPortfoliosMock) {
335-
countUserPortfoliosMock.countByUserId(UserId("bob"))
336-
}
337-
} returns 0
356+
every { countUserPortfoliosMock.countByUserId(UserId("bob")) } returns 0
338357
```
339358

340-
The above code translates to the following sentence: "Every time we call the method `countByUserId` of the instance `countUserPortfoliosMock` with input equals to `UserId("bob")`, we'll get the value `0` as a result.". Despite that, we get an error if we try to compile it:
341-
342-
```
343-
No context receiver for 'arrow.core.raise.Raise<in.rcard.arrow.raise.testing.DomainError>' found.
344-
```
345-
346-
The compiler tells the truth. We defined the `countByUserId` function using the `Raise<DomainError>` context. We should remember that **declaring a context receiver is like adding an implicit input parameter to the list of explicitly declared input parameters**. So, the compiler tells us we're not giving enough parameters for the function to be mocked.
347-
348-
We need to add the missing parameter with a matcher, as with any other input parameter. The only difference is that we need the `Raise<DomainError>` at the scope level. Then, we can use the `with` scope function once again:
349-
350-
```kotlin
351-
every {
352-
with(any<Raise<DomainError>>()) {
353-
with(countUserPortfoliosMock) {
354-
countByUserId(UserId("bob"))
355-
}
356-
}
357-
} returns 0
358-
```
359-
360-
Now, the compiler is happier, and we can proceed with the rest of the test code:
359+
Then, we can proceed with the rest of the test code:
361360

362361
```kotlin
363362
should("create a portfolio for a user (using mockk") {
364363
val countUserPortfoliosMock: CountUserPortfoliosPort = mockk()
365364
val underTestWithMock = createPortfolioUseCase(countUserPortfoliosMock)
366-
every {
367-
with(any<Raise<DomainError>>()) {
368-
with(countUserPortfoliosMock) {
369-
countByUserId(UserId("bob"))
370-
}
371-
}
372-
} returns 0
365+
every { countUserPortfoliosMock.countByUserId(UserId("bob")) } returns 0
366+
373367
val actualResult: Either<DomainError, PortfolioId> =
374368
either {
375369
with(underTestWithMock) {
@@ -446,6 +440,8 @@ The above test verifies the behavior of the use case when there was an unexpecte
446440
For completeness, we can translate the above test using Mockito to understand the differences between the two libraries. In detail, we'll use the library `mockito-kotlin` on top of Mockito to have a more idiomatic look and feel:
447441

448442
```kotlin
443+
import org.mockito.kotlin.*
444+
449445
@Test
450446
fun `given a userId and an initial amount, when executed with error, then propagates the error properly`() {
451447
val exception = RuntimeException("Ooops!")

0 commit comments

Comments
 (0)