Skip to content

Commit

Permalink
Merge pull request #9 from adidas/enhancement/Replace-mockk-with-Test…
Browse files Browse the repository at this point in the history
…-doubles

Enhancement/replace mockk with test doubles
  • Loading branch information
sherviiin authored Aug 3, 2022
2 parents 7941814 + 67eb8d2 commit e5153dd
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 84 deletions.
2 changes: 0 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
[versions]
kotlin = "1.6.21"
coroutines = "1.6.1"
mockk = "1.12.4"
kotest = "5.3.0"
ktlint = "0.45.2"

[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }

[plugins]
Expand Down
1 change: 0 additions & 1 deletion mvi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,5 @@ dependencies {
implementation(libs.coroutines.core)

testImplementation(libs.kotest.runner)
testImplementation(libs.mockk)
testImplementation(libs.coroutines.test)
}
108 changes: 63 additions & 45 deletions mvi/src/test/kotlin/com/adidas/mvi/reducer/ReducerTests.kt
Original file line number Diff line number Diff line change
@@ -1,41 +1,48 @@
package com.adidas.mvi.reducer

import com.adidas.mvi.CoroutineListener
import com.adidas.mvi.Logger
import com.adidas.mvi.Reducer
import com.adidas.mvi.SimplifiedIntentExecutor
import com.adidas.mvi.TerminatedIntentException
import com.adidas.mvi.reducer.logger.SpyLogger
import com.adidas.mvi.reducer.logger.shouldContainFailingIntent
import com.adidas.mvi.reducer.logger.shouldContainFailingTransform
import com.adidas.mvi.reducer.logger.shouldContainSuccessfulIntent
import com.adidas.mvi.reducer.logger.shouldContainSuccessfulTransform
import com.adidas.mvi.transform.StateTransform
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.inspectors.shouldForAtLeastOne
import io.kotest.inspectors.shouldForNone
import io.kotest.inspectors.shouldForOne
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope

internal class ReducerTests : BehaviorSpec({

isolationMode = IsolationMode.InstancePerLeaf

val intentExecutor = mockk<SimplifiedIntentExecutor<TestIntent, TestState>>()
val logger = mockk<Logger>(relaxUnitFun = true)

val logger = SpyLogger()
val coroutineListener = CoroutineListener()
listeners(coroutineListener)

fun createReducer() = Reducer(
fun createReducer(executor: (intent: TestIntent) -> Flow<StateTransform<TestState>> = createIntentExecutorContainer(transform = TestTransform.Transform1)) = Reducer(
coroutineScope = TestScope(coroutineListener.testCoroutineDispatcher),
initialState = TestState.InitialState,
defaultDispatcher = coroutineListener.testCoroutineDispatcher,
logger = logger,
intentExecutor = intentExecutor
intentExecutor = executor
)

Given("A reducer with a initial state") {
val reducer = createReducer()
val executedIntents = mutableListOf<TestIntent>()
val executor: (intent: TestIntent) -> Flow<StateTransform<TestState>> = createIntentExecutorContainer(executedIntents)
val reducer = createReducer(executor)

When("I listen to its first value") {
val firstValue = reducer.state.value
Expand Down Expand Up @@ -65,74 +72,67 @@ internal class ReducerTests : BehaviorSpec({
reducer.executeIntent(TestIntent.SimpleIntent)

Then("This intent should be logged") {
verify(exactly = 1) { logger.logIntent(TestIntent.SimpleIntent) }
verify(exactly = 1) { intentExecutor.invoke(TestIntent.SimpleIntent) }
logger.history.shouldForOne { log ->
log shouldContainSuccessfulIntent TestIntent.SimpleIntent.toString()
}
executedIntents shouldContain TestIntent.SimpleIntent
}
}
}

Given("A reducer which triggers a TerminatedIntentException when executing a SimpleIntent") {
val exceptionToThrow = TerminatedIntentException()
every { intentExecutor.invoke(TestIntent.SimpleIntent) } throws exceptionToThrow
val reducer = createReducer()
val reducer = createReducer(createIntentExecutorContainer(exception = exceptionToThrow))

When("I execute an intent and it is cancelled throwing TerminatedIntentException") {
reducer.executeIntent(TestIntent.SimpleIntent)

Then("The exception should NOT be logged") {
verify(exactly = 0) { logger.logFailedIntent(TestIntent.SimpleIntent, exceptionToThrow) }
logger.history.shouldForNone { log ->
log shouldContainSuccessfulIntent exceptionToThrow.toString()
}
}
}
}

Given("A reducer which fails to execute a SimpleIntent") {
val reducer = createReducer()

val exceptionToThrow = Exception()
every { intentExecutor.invoke(TestIntent.SimpleIntent) } throws exceptionToThrow
val reducer = createReducer(createIntentExecutorContainer(exception = exceptionToThrow))

When("I execute an intent") {
reducer.executeIntent(TestIntent.SimpleIntent)

Then("The exception should be logged") {
verify(exactly = 1) { logger.logFailedIntent(TestIntent.SimpleIntent, exceptionToThrow) }
logger.history.shouldForAtLeastOne { log ->
log shouldContainFailingIntent exceptionToThrow.toString()
}
}
}
}

Given("A reducer which produces Transform1 partial state when Transform1Producer intent is sent") {
val reducer = createReducer()

every {
intentExecutor.invoke(
TestIntent.Transform1Producer,
val reducer = createReducer(
createIntentExecutorContainer(
intent = TestIntent.Transform1Producer,
transform = TestTransform.Transform1
)
} returns flowOf(TestTransform.Transform1)
)

When("I execute Transform1Producer intent") {
reducer.executeIntent(TestIntent.Transform1Producer)

Then("The state should change to the state which Transform1 produces") {
reducer.state.value shouldBe TestState.StateFromTransform1
verify(exactly = 1) {
logger.logTransformedNewState(
transform = TestTransform.Transform1,
previousState = TestState.InitialState,
newState = TestState.StateFromTransform1
)
logger.history.shouldForOne { log ->
log shouldContainSuccessfulTransform TestState.StateFromTransform1.toString()
}
}
}
}

Given("A reducer which produces FailedTransform partial state when FailedTransformProducer intent is sent") {
val reducer = createReducer()

every {
intentExecutor.invoke(
TestIntent.FailedTransformProducer
)
} returns flowOf(TestTransform.FailedTransform)
val transform = TestTransform.FailedTransform
val reducer = createReducer(createIntentExecutorContainer(transform = transform))

When("I execute an FailedTransformProducer intent") {
reducer.executeIntent(TestIntent.FailedTransformProducer)
Expand All @@ -142,12 +142,8 @@ internal class ReducerTests : BehaviorSpec({
}

Then("The failure should be logged") {
verify(exactly = 1) {
logger.logFailedTransformNewState(
TestTransform.FailedTransform,
TestState.InitialState,
ofType<Exception>()
)
logger.history.shouldForOne { log ->
log shouldContainFailingTransform transform.toString()
}
}
}
Expand Down Expand Up @@ -182,3 +178,25 @@ internal class ReducerTests : BehaviorSpec({
}
}
})

private fun createIntentExecutorContainer(executedIntents: MutableList<TestIntent> = mutableListOf()): (TestIntent) -> Flow<StateTransform<TestState>> =
{
executedIntents.add(it)
flowOf(TestTransform.Transform1)
}

private fun createIntentExecutorContainer(exception: java.lang.Exception): (TestIntent) -> Flow<StateTransform<TestState>> =
{
if (it is TestIntent.SimpleIntent) throw exception
emptyFlow()
}

private fun createIntentExecutorContainer(intent: TestIntent = TestIntent.FailedTransformProducer, transform: TestTransform): (TestIntent) -> Flow<StateTransform<TestState>> =
{
when (it) {
intent -> {
flowOf(transform)
}
else -> emptyFlow()
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.adidas.mvi.reducer

import com.adidas.mvi.CoroutineListener
import com.adidas.mvi.Logger
import com.adidas.mvi.Reducer
import io.mockk.mockk
import com.adidas.mvi.reducer.logger.SpyLogger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
Expand All @@ -20,7 +19,7 @@ internal class TestCancellationReducerWrapper(
coroutineScope = TestScope(coroutineListener.testCoroutineDispatcher),
initialState = TestState.InitialState,
defaultDispatcher = coroutineListener.testCoroutineDispatcher,
logger = mockk<Logger>(relaxUnitFun = true),
logger = SpyLogger(),
intentExecutor = this::executeIntent
)
val state = reducer.state
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.adidas.mvi.reducer.logger

import io.kotest.matchers.string.shouldContainInOrder

infix fun String?.shouldContainSuccessfulIntent(substr: String): String? {
this.shouldContainInOrder(SUCCESSFUL_INTENT, substr)
return this
}

infix fun String?.shouldContainFailingIntent(substr: String): String? {
this.shouldContainInOrder(FAILED_INTENT, substr)
return this
}

infix fun String?.shouldContainSuccessfulTransform(substr: String): String? {
this.shouldContainInOrder(SUCCESSFUL_TRANSFORM, substr)
return this
}

infix fun String?.shouldContainFailingTransform(substr: String): String? {
this.shouldContainInOrder(FAILED_TRANSFORM, substr)
return this
}
67 changes: 67 additions & 0 deletions mvi/src/test/kotlin/com/adidas/mvi/reducer/logger/SpyLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.adidas.mvi.reducer.logger

import com.adidas.mvi.Loggable
import com.adidas.mvi.Logger
import java.lang.StringBuilder

private const val SPACE = " "
internal const val SUCCESSFUL_INTENT = "SuccessfulIntent:"
internal const val FAILED_INTENT = "FailedIntent:"
internal const val SUCCESSFUL_TRANSFORM = "SuccessfulTransform:"
internal const val FAILED_TRANSFORM = "FailedTransform:"

class SpyLogger : Logger {

var history = mutableListOf<String>()

override fun logIntent(intent: Loggable) {
log(intent.toString())
log(
StringBuilder().apply {
append(SUCCESSFUL_INTENT)
append(intent.toString())
}.toString()
)
}

override fun logFailedIntent(intent: Loggable, throwable: Throwable) {
log(
StringBuilder().apply {
append(FAILED_INTENT)
append(intent.toString())
append(SPACE)
append(throwable)
}.toString()
)
}

override fun logTransformedNewState(transform: Loggable, previousState: Loggable, newState: Loggable) {
log(
StringBuilder().apply {
append(SUCCESSFUL_TRANSFORM)
append(transform.toString())
append(SPACE)
append(previousState.toString())
append(SPACE)
append(newState.toString())
}.toString()
)
}

override fun logFailedTransformNewState(transform: Loggable, state: Loggable, throwable: Throwable) {
log(
StringBuilder().apply {
append(FAILED_TRANSFORM)
append(transform.toString())
append(SPACE)
append(state.toString())
append(SPACE)
append(throwable)
}.toString()
)
}

private fun log(message: String) {
history.add(message)
}
}
Loading

0 comments on commit e5153dd

Please sign in to comment.