Skip to content

Commit

Permalink
Merge pull request #1141 from square/sedwards/test-render-session
Browse files Browse the repository at this point in the history
Add testRender with CoroutineScope for SessionWorkflow
  • Loading branch information
steve-the-edwards authored Dec 13, 2023
2 parents 3d54f7b + c43729f commit 785da30
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 1 deletion.
1 change: 1 addition & 0 deletions workflow-testing/api/workflow-testing.api
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public final class com/squareup/workflow1/testing/RenderTesterKt {
public static synthetic fun expectWorkflow$default (Lcom/squareup/workflow1/testing/RenderTester;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
public static synthetic fun expectWorkflow$default (Lcom/squareup/workflow1/testing/RenderTester;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
public static synthetic fun expectWorkflow$default (Lcom/squareup/workflow1/testing/RenderTester;Lkotlin/reflect/KClass;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
public static final fun testRender (Lcom/squareup/workflow1/SessionWorkflow;Ljava/lang/Object;Lkotlinx/coroutines/CoroutineScope;)Lcom/squareup/workflow1/testing/RenderTester;
public static final fun testRender (Lcom/squareup/workflow1/StatefulWorkflow;Ljava/lang/Object;Ljava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
public static final fun testRender (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.squareup.workflow1.testing

import com.squareup.workflow1.ActionApplied
import com.squareup.workflow1.SessionWorkflow
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.WorkflowAction
import com.squareup.workflow1.WorkflowExperimentalApi
import com.squareup.workflow1.WorkflowIdentifier
import com.squareup.workflow1.WorkflowOutput
import com.squareup.workflow1.identifier
import com.squareup.workflow1.testing.RenderTester.ChildWorkflowMatch
import com.squareup.workflow1.workflowIdentifier
import kotlinx.coroutines.CoroutineScope
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection
Expand All @@ -19,14 +22,40 @@ import kotlin.reflect.KTypeProjection
*
* See [RenderTester] for usage documentation.
*/
@OptIn(WorkflowExperimentalApi::class) // Opt-in is only for the argument check.
@Suppress("UNCHECKED_CAST")
public fun <PropsT, OutputT, RenderingT> Workflow<PropsT, OutputT, RenderingT>.testRender(
props: PropsT
): RenderTester<PropsT, *, OutputT, RenderingT> {
val statefulWorkflow = asStatefulWorkflow() as StatefulWorkflow<PropsT, Any?, OutputT, RenderingT>
return statefulWorkflow.testRender(
props = props,
initialState = statefulWorkflow.initialState(props, null)
initialState = run {
require(this !is SessionWorkflow<PropsT, *, OutputT, RenderingT>) {
"Called testRender on a SessionWorkflow without a CoroutineScope. Use the version that passes a CoroutineScope."
}
statefulWorkflow.initialState(props, null)
}
) as RenderTester<PropsT, Nothing, OutputT, RenderingT>
}

/**
* Create a [RenderTester] to unit test an individual render pass of this [SessionWorkflow],
* using the workflow's [initial state][StatefulWorkflow.initialState], in the [workflowScope].
*
* See [RenderTester] for usage documentation.
*/
@OptIn(WorkflowExperimentalApi::class)
@Suppress("UNCHECKED_CAST")
public fun <PropsT, OutputT, RenderingT> SessionWorkflow<PropsT, *, OutputT, RenderingT>.testRender(
props: PropsT,
workflowScope: CoroutineScope
): RenderTester<PropsT, *, OutputT, RenderingT> {
val sessionWorkflow: SessionWorkflow<PropsT, Any?, OutputT, RenderingT> =
asStatefulWorkflow() as SessionWorkflow<PropsT, Any?, OutputT, RenderingT>
return sessionWorkflow.testRender(
props = props,
initialState = sessionWorkflow.initialState(props, null, workflowScope)
) as RenderTester<PropsT, Nothing, OutputT, RenderingT>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.squareup.workflow1.Worker
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.WorkflowAction
import com.squareup.workflow1.WorkflowAction.Companion.noAction
import com.squareup.workflow1.WorkflowExperimentalApi
import com.squareup.workflow1.WorkflowIdentifier
import com.squareup.workflow1.WorkflowOutput
import com.squareup.workflow1.action
Expand All @@ -19,14 +20,18 @@ import com.squareup.workflow1.identifier
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.rendering
import com.squareup.workflow1.runningWorker
import com.squareup.workflow1.sessionWorkflow
import com.squareup.workflow1.stateful
import com.squareup.workflow1.stateless
import com.squareup.workflow1.testing.RenderTester.ChildWorkflowMatch.Matched
import com.squareup.workflow1.unsnapshottableIdentifier
import com.squareup.workflow1.workflowIdentifier
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.test.runTest
import org.mockito.kotlin.mock
import kotlin.reflect.typeOf
import kotlin.test.Test
Expand Down Expand Up @@ -1263,6 +1268,95 @@ internal class RealRenderTesterTest {
assertEquals(2, renderCount)
}

@OptIn(WorkflowExperimentalApi::class)
@Test
fun `testRender with SessionWorkflow throws exception`() {
class TestAction : WorkflowAction<Unit, String, String>() {
override fun Updater.apply() {
state = "new state"
setOutput("output")
}
}

val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
initialState = { _, _: CoroutineScope -> "initial" },
render = { _, _ ->
actionSink.contraMap { it }
}
)

val exception = assertFailsWith<IllegalArgumentException> {
workflow.testRender(Unit)
.render { sink ->
sink.send(TestAction())
}
}

assertEquals(
exception.message,
"Called testRender on a SessionWorkflow without a CoroutineScope. Use the version that passes a CoroutineScope."
)
}

@OptIn(WorkflowExperimentalApi::class)
@Test
fun `testRender with CoroutineScope works for SessionWorkflow`() = runTest {
class TestAction : WorkflowAction<Unit, String, String>() {
override fun Updater.apply() {
state = "new state"
setOutput("output")
}
}

val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
initialState = { _, _: CoroutineScope -> "initial" },
render = { _, _ ->
actionSink.contraMap { it }
}
)

val testResult = workflow.testRender(Unit, this)
.render { sink ->
sink.send(TestAction())
}

testResult.verifyActionResult { state, output ->
assertEquals("new state", state)
assertEquals("output", output?.value)
}
}

@OptIn(WorkflowExperimentalApi::class)
@Test
fun `testRender with CoroutineScope uses the correct scope`() = runTest {
val signalMutex = Mutex(locked = true)
class TestAction : WorkflowAction<Unit, String, String>() {
override fun Updater.apply() {
state = "new state"
setOutput("output")
}
}

val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
initialState = { _, workflowScope: CoroutineScope ->
assertEquals(workflowScope, this@runTest)
signalMutex.unlock()
"initial"
},
render = { _, _ ->
actionSink.contraMap { it }
}
)

workflow.testRender(Unit, this)
.render { sink ->
sink.send(TestAction())
}

// Assertion happens in the `initialState` call above.
signalMutex.lock()
}

@Test fun `createRenderChildInvocation() for Workflow-stateless{}`() {
val workflow = Workflow.stateless<String, Int, Unit> {}
val invocation = createRenderChildInvocation(workflow, "props", "key")
Expand Down

0 comments on commit 785da30

Please sign in to comment.