How to Write Integration Tests for Old Workflow Versions Using Workflow.getVersion in Java SDK

Hello,

I’m looking for guidance on how to write integration tests that cover old versions of a workflow, specifically in the Java SDK.

We currently use Workflow Replayer tests frequently to validate old versions, but they are not helpful for all scenarios.

Example Scenario:

Let’s say this was the original version of our workflow:

MyWorkflow {
  step1();
  step2();
  step3();
  step4();
}

Now, we’ve updated the workflow as follows:

MyWorkflow {
  int version = Workflow.getVersion("change", Workflow.DEFAULT_VERSION, 1);
  
  if (version == Workflow.DEFAULT_VERSION) {
    step1();
    step2();
    step3();
    step4();
  } else {
    step1New();
    step2New();
    step3New();
    step4New();
    step5();
  }
}

I want to ensure that existing workflow runs (which started with the old code) continue executing the old logic, regardless of code changes.


The Problem:

Currently, integration tests always run the latest version of the workflow. I need a way to write tests that specifically execute the old code path.

Here’s a concrete example of where Replayer tests fall short:

Suppose we have a workflow (RunId: 1) running on the old version and it is currently paused at step2(). While updating the code, I mistakenly introduce a bug and omit step4() from the old version logic:

MyWorkflow {
  int version = Workflow.getVersion("change", Workflow.DEFAULT_VERSION, 1);
  
  if (version == Workflow.DEFAULT_VERSION) {
    step1();
    step2();
    step3(); // step4() mistakenly removed!
  } else {
    step1New();
    step2New();
    step3New();
    step4New();
    step5();
  }
}

This change is incorrect, but Replayer tests will not catch it because they only verify that the history can be replayed—they do not ensure that the old logic is still intact.


Question:

Is there any way to write integration tests that explicitly test the old version path of a workflow (i.e., where Workflow.getVersion(...) returns DEFAULT_VERSION)?

Ideally, I’d like to be able to mock or force Workflow.getVersion to return the old version, but that doesn’t seem currently possible.

How do others handle this kind of testing, especially when ensuring that older, in-progress workflows continue to function correctly after workflow updates?

Thanks in advance!

You could add a test interceptor and intercept getVersion calls to explicitly set to default when you want to test that path, then replay it against worker from test env that has interceptor which does not explicitly sets to default and applies your code getVersion path

See maybe test here for reference if it helps.

ensuring that older, in-progress workflows continue to function correctly after workflow updates?

This part is a bit harder as execution and event history is not complete yet. You will need a completed execution history to make this comparison I think, and if you looked at linked test it shows how you can run execution in-test and get its event history, if you don’t have any completed ones on real cluster. This can also be useful to generate event histories for all possible business logic paths of your workflow impl, which might be harder to gather from real executions.

Thanks for your reply. I was able to use TestWorkerInterceptor to force a specific version of my Workflow to start in my WorkflowIntegrationTest. I prefer having two test cases for my workflow—one that tests the old version and another that tests the new version (each using a different TestEnvironment). I prefer writing integration tests with a mocked getVersion rather than relying solely on Replayer tests to validate old behavior. This way, if we miss an activity while copying the old version, it will be caught in an integration test.

What we currently do to test old versions is implement a custom context propagator that supports MockStatic in Temporal Workflow, ensuring that our stubbed behavior isn’t lost upon switching threads. We use this to mock Workflow.getVersion along with other methods. One of our team members even wrote an article that explains our approach in detail:
Mock Static Methods in Temporal Workflow Tests.

Questions:

  • What do you think about this approach? It has been working well for us so far.
  • Are there any plans for Temporal to support such use cases for testing natively?

It has been working well for us so far.

Glad to hear, thanks for sharing the blog. Can you show how you currently use this approach for the mentioned tests in your initial post?

rather than relying solely on Replayer tests to validate old behavior

Replayer is imo best suited for testing your different versions before your deploy your change in production, maybe you could use it in combination with your context propagator approach to test all possible execution branches?

This is how we currently do it.
The code would be like this.

public class MyWorkflowImpl implements MyWorkflow {
  @Override
  public void execute() {
    int version = WorkflowVersion.getVersion("change", Workflow.DEFAULT_VERSION, 1);
    if (version == Workflow.DEFAULT_VERSION) {
      activity1.call();
      activity2.call();
      activity3.call();
    } else {
      activity1New.call();
      activity2New.call();
      activity3New.call();
    }
  }
}

In our workflow integration test


public class MyWorkflowIntegrationTest {

    // Our custom interface & annotation for mocking static methods.
    @CustomMockAnnotation(WorkflowVersion.class)
    private MockStatic workflowVersion;

    private TestEnvironment testEnv;

    @Before
    public void setup() {
        // Initialize TestEnvironment with custom context propagators
        TestEnvironmentOptions options = getTestEnvironmentOptions();
        testEnv = TestEnvironment.newInstance(options);
        testEnv.start();
    }

    
    @Test
    public void Should_DoSomething_WhenVersionIsDefault() {
        // Arrange: Stub getVersion to return the default version,
        // ensuring the old behavior is executed.
        workflowVersion.when(() -> WorkflowVersion.getVersion("change", Workflow.DEFAULT_VERSION, 1))
            .thenAnswer(invocation -> Workflow.DEFAULT_VERSION);

        // Act: Start the workflow using a stub from the TestEnvironment.
        MyWorkflow workflow = testEnv.getWorkflowClient()
            .newWorkflowStub(MyWorkflow.class);
        workflow.execute();

        // Assert: Verify that the old activities (activity1, activity2, activity3) were called.
    }

    @Test
    public void Should_DoAnotherThing_WhenVersionIsNotDefault() {
        // Arrange: Stub getVersion to return the new version,
        // ensuring the new behavior is executed.
        workflowVersion.when(() -> WorkflowVersion.getVersion("change", Workflow.DEFAULT_VERSION, 1))
            .thenAnswer(invocation -> 1);

        // Act: Start the workflow using a stub from the TestEnvironment.
        MyWorkflow workflow = testEnv.getWorkflowClient()
            .newWorkflowStub(MyWorkflow.class);
        workflow.execute();

        // Assert: Verify that the new activities (activity1New, activity2New, activity3New) were called.
    }
}

This approach enables us to have 100% coverage for the Workflow with all the branches tested in an integration test. We still use ReplayerTest but they are to check if we broke determinism with our new code.

Thanks for sharing, can you use the same approach with the mentioned test interceptor instead? Seems to probably be doing very similar thing?

Yes, that’s correct. We still use MockStatic for other use cases beyond just mocking versions. For example, in our WorkflowIntegrationTests we also mock Activity RetryOptions for specific tests (like simulating a timeout).

Below is an example of how our workflow is initialized:

class MyWorkflow {
  private final MyActivity myActivity = Workflow.newActivityStub(
      MyActivity.class, MyActivity.getActivityOptions()
  );
}

And here’s the activity interface:

public interface MyActivity {

    static ActivityOptions getDefaultActivityOptions() {
        RetryOptions retryOptions = RetryOptions.newBuilder()
            .setInitialInterval(Duration.ofSeconds(30))
            .setBackoffCoefficient(1)
            .setMaximumAttempts(3)
            .build();

        return ActivityOptions.newBuilder()
            .setStartToCloseTimeout(Duration.ofSeconds(300))
            .setRetryOptions(retryOptions)
            .build();
    }

    @ActivityMethod
    MyActivityResponse execute(MyActivityRequest request);
}

In our test, we control the activity options using MockStatic. For example:

class WorkflowIntegrationTest {

    @CustomMockAnnotation(MyActivity.class)
    private MockStatic myActivity;

    @Test
    void Should_HandleError_WhenActivityTimesout() {
        // Mock Activity.getActivityOptions to return custom options with a short timeout
        myActivity.when(() -> MyActivity.getActivityOptions())
            .thenAnswer(invocation -> getCustomActivityOptionsWithShortTimeout());

        // Simulate the activity execution timing out
        doAnswer(invocation -> {
            testWorkflowEnvironment.sleep(Duration.ofSeconds(2));
            return null;
        }).when(myActivity).execute();

        // ... rest of test logic ...
    }
}

We have other cases where we mock ActivityOptions similarly. Is there another way to not only register mocked activities but also control the activity options directly within tests?

Maybe we could use the same TestInterceptor approach for Workflow.newActivityStub?
I’m trying to see if we can avoid MockStatic in our tests while making sure we can still test all the use cases we have currently easily.

Thank you for replies, much appreciated!