Throwing exception in mocked activity hangs the test

I can’t figure out what I am doing wrong. My tests work fine when mocking happy path, but when throwing an error on a mocked activity the test hangs indefinitely. I tried different combinations of retry configurations, and setting setFailWorkflowExceptionTypes for the workflow.

import io.temporal.client.WorkflowException
import io.temporal.client.WorkflowOptions
import io.temporal.failure.ActivityFailure
import io.temporal.failure.ApplicationFailure
import io.temporal.failure.ChildWorkflowFailure
import io.temporal.testing.TestWorkflowEnvironment
import io.temporal.testing.TestWorkflowExtension
import io.temporal.worker.Worker
import io.temporal.worker.WorkflowImplementationOptions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
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 worker = testEnv.newWorker(taskQueue);
    private val opts = WorkflowImplementationOptions.newBuilder()
        .setFailWorkflowExceptionTypes(
            Throwable::class.java,
        )
//        .setDefaultActivityOptions(
//            ActivityOptions
//                .newBuilder()
//                .setStartToCloseTimeout(Duration.ofSeconds(1))
//                .setRetryOptions(
//                    RetryOptions.newBuilder()
//                        .setMaximumAttempts(1)
//                        .setInitialInterval(Duration.ofMillis(10))
//                        .setBackoffCoefficient(1.0)
//                        .build()!!
//                ).build()
//        )
//        .setDefaultLocalActivityOptions(
//            LocalActivityOptions.newBuilder()
//                .setStartToCloseTimeout(Duration.ofSeconds(1))
//                .setRetryOptions(
//                    RetryOptions.newBuilder()
//                        .setMaximumAttempts(1)
//                        .setInitialInterval(Duration.ofMillis(10))
//                        .setBackoffCoefficient(1.0)
//                        .build()!!
//                ).build()
//        )
        .build()!!

    @Test
    fun testMockedExceptionGetGreeting() {
        val formatActivities = mock<HelloWorldActivities>()
        whenever(formatActivities.composeGreeting(anyString())).thenThrow(NullPointerException("test error"))
        worker.registerActivitiesImplementations(formatActivities)
        worker.registerWorkflowImplementationTypes(
            opts,
            HelloWorldWorkflowImpl::class.java
        )
        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 ChildWorkflowFailure)
            assertTrue(e.cause?.cause is ActivityFailure)
            assertTrue(e.cause?.cause?.cause is ApplicationFailure)
            assertEquals(
                "test error",
                (e.cause?.cause?.cause as ApplicationFailure).originalMessage
            )
        }
    }
}

package versions:

    implementation("io.temporal:temporal-sdk:1.22.3")
    testImplementation("io.temporal:temporal-testing:1.22.3")
    testImplementation(platform("org.junit:junit-bom:5.10.1"))
    testImplementation("org.junit.jupiter:junit-jupiter")

I guess that the activity is retried forever. I would recommend specifying the test timeout through @Test annotation.

You are right. Is there a built-in way to override or mock the ActivityOptions specified when stubbing activity within Workflow? I got a perception that WorkflowImplementationOptions.setActivityOptions has highest precedence and would do that, but it is clearly not the case.

As I had to explicitly specify retry=1 policy in code to make workflow fail:

class HelloWorldWorkflowImpl : HelloWorldWorkflow {

    private val activityOptions = ActivityOptions.newBuilder()
        .setStartToCloseTimeout(Duration.ofSeconds(60))
        .setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(1).validateBuildWithDefaults())
        .validateAndBuildWithDefaults()!!

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

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

}

I though about this, but what would be tested in this case? If an activity has infinite retries, then workflow will never receive its failure.

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.

I got a wrong impression that WorkflowImplementationOptions has a way to override the activity options set during Workflow.newActivityStub(Class, ActivityOptions) . Right now ended up using some flagging to the code that it is being executed by a test and it will use a different configuration.

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.

These are good points. Would you file a feature request against the sdk-java?

Filed one Have a built-in way to override activityOptions in tests · Issue #1988 · temporalio/sdk-java · GitHub