temporalio / sdk-java

Temporal Java SDK
https://temporal.io
Apache License 2.0
200 stars 134 forks source link

Have a built-in way to override activityOptions in tests #1988

Open yunmanger1 opened 4 months ago

yunmanger1 commented 4 months ago

Is your feature request related to a problem? Please describe. Request: Make it possible for test setup to override ActivityOptions set for activities in workflow.

Problem: In production code I have high retry count on activities, but in testing I want to override that to have maxAttempts=1 so that workflow gets it’s failure right away. Or in case I didn’t mock some activity properly the test will fail right away with NullPointerException instead of hanging indefinitely.

Another thing is that, when stubbing activities in workflow I hardcode the queue name, because it might not be the same the workflow is on. And in tests I need to override it to make things run, without reproducing all the queue-worker combinations in prod.

Describe the solution you'd like

Sth like overrideAllActivityOptions or overrideActivityOptions in example below.

import io.temporal.activity.ActivityOptions
import io.temporal.client.WorkflowException
import io.temporal.client.WorkflowOptions
import io.temporal.common.RetryOptions
import io.temporal.failure.ActivityFailure
import io.temporal.failure.ApplicationFailure
import io.temporal.testing.TestWorkflowEnvironment
import io.temporal.worker.WorkflowImplementationOptions
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

class HelloWorldWorkflowImplTest {

    private val taskQueue = "TASK_QUEUE"
    private val testEnv = TestWorkflowEnvironment.newInstance();
    private val testActivityOptions = ActivityOptions.newBuilder()
        .setTaskQueue(taskQueue)
        .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).validateBuildWithDefaults())
        .validateAndBuildWithDefaults()
    private val opts = WorkflowImplementationOptions.newBuilder()
        .setFailWorkflowExceptionTypes(
            Throwable::class.java,
        )
        .setDefaultActivityOptions(
            testActivityOptions
        )
        .overrideAllActivityOptions(
            testActivityOptions
        )
        .overrideActivityOptions(
            mapOf(HelloWorldActivities::class.java to testActivityOptions)
        )
        .build()!!
    private val worker = testEnv.newWorker(taskQueue).also {
        it.registerWorkflowImplementationTypes(
            opts,
            HelloWorldWorkflowImpl::class.java
        )
    }

    @Test
    fun `activity error should fail workflow`() {
        val formatActivities = mock<HelloWorldActivities>()
        var count = 1
        whenever(formatActivities.composeGreeting(anyString())).then {
            println("CALL $count")
            count += 1
            error("test error")
        }
        worker.registerActivitiesImplementations(formatActivities)
        testEnv.start()

        val workflow = testEnv.workflowClient.newWorkflowStub(
            HelloWorldWorkflow::class.java,
            WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build()!!
        )
        try {
            workflow.getGreeting("Mock")
            error("unreachable")
        } catch (e: WorkflowException) {
            assertTrue(e.cause is ActivityFailure)
            assertTrue(e.cause?.cause is ApplicationFailure)
            assertEquals(
                "test error",
                (e.cause?.cause as ApplicationFailure).originalMessage
            )
        }
    }
}

Describe alternatives you've considered Right now I am flagging to code directly to use testActivityOptions when executed from test.

    @BeforeEach
    fun `setup overrides`() {
        GlobalWorkflowOptions.set(testActivityOptions)
    }

    @AfterEach
    fun `reset overrides`() {
        GlobalWorkflowOptions.clear()
    }

and in code

class HelloWorldWorkflowImpl : HelloWorldWorkflow {

    private val activityOptions = ActivityOptions.newBuilder()
        .setTaskQueue(HELLO_WORLD_TASK_QUEUE)
        .setStartToCloseTimeout(Duration.ofSeconds(60))
        .validateAndBuildWithDefaults()!!

    private val activity = Workflow.newActivityStub(
        HelloWorldActivities::class.java,
        GlobalWorkflowOptions.activityOptions(activityOptions)
    )

    override fun getGreeting(name: String): String {
        return activity.composeGreeting(name)
    }

}

Additional context https://community.temporal.io/t/throwing-exception-in-mocked-activity-hangs-the-test/10932

Related issues: #499 #626

cretz commented 4 months ago

In the meantime, can you consider having your mock activity throw a non-retryable exception?