temporalio / temporalio/sdk-java

Have a built-in way to override activityOptions in tests

Open
#1,988 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Java
Stars
433
Forks
249
Avg merge
5d 6h
Merged PRs (30d)
26

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with WorkflowImplementationOptions and the TestWorkflowEnvironment worker registration shown in the example, then trace how activity options are selected for workflow activity stubs. Define and verify behavior for default, all-activity, and per-activity overrides, including retry limits and task queues, using the failure scenario described in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend-api-design, testing
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.