Unit testing Workflow interface implementation that contains @Autowired

Hi Temporal team, I am having some trouble unit testing a Workflow class. I’m using Spring Boot, Junit 5, and Mockito.
This is my Workflow class:

@Component
@WorkflowImpl(taskQueues = "TaskQueue")
public class UpgradeInstanceWorkflowImpl implements UpgradeInstanceWorkflow {
  @Autowired Util util;

  @Override
  public String beginUpgradeInstance(UpgradeRequest upgradeRequest) {
    // Configure activity
    long activityTimeout = 0;
    activityTimeout = util.getConfigurationCheckTimeout(temporalActivityTimeoutConfig);
    UpgradeInstanceActivity upgradeInstanceActivity =
        Workflow.newActivityStub(
            UpgradeInstanceActivity.class,
            ActivityOptions.newBuilder()
                .setScheduleToStartTimeout(Duration.ofMinutes(10))
                .setStartToCloseTimeout(Duration.ofHours(activityTimeout))
                .setRetryOptions(upgradeRetryOptions)
                .build());
    upgradeInstanceActivity.backupJenkinsMasterActivity(upgradeRequest);
}

As you can see I have an @Autowired Util util; variable because I need to use a function from this class.

My unit test looks like this:

@ExtendWith(SpringExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SpringBootTest
@ContextConfiguration(classes = {TemporalTestConfig.class})
@Profile("test")
public class UpgradeInstanceWorkflowTest {
  @MockBean Util util;
  @Autowired UpgradeInstanceActivity activity;

  @RegisterExtension
  public final TestWorkflowExtension testWorkflowExtension =
      TestWorkflowExtension.newBuilder()
          .setWorkflowTypes(UpgradeInstanceWorkflowImpl.class)
          .setDoNotStart(true)
          .build();
  @Test
  public void testBeginUpgradeInstance(
      TestWorkflowEnvironment testWorkflowEnvironment,
      Worker worker,
      UpgradeInstanceWorkflow workflow)
      throws InterruptedException {
    // Mock activities
    UpgradeInstanceActivity activities =
        mock(activity.getClass(), withSettings().withoutAnnotations());
    doNothing().when(activities).backupJenkinsMasterActivity(any());
    doNothing().when(activities).scheduleUpgradeActivity(any());
    doNothing().when(activities).checkUpgradeRequestStatusActivity(any());
    
    // Register with worker
    worker.registerActivitiesImplementations(activities);

    // Start test environment
    testWorkflowEnvironment.start();

    int workflowTimeout = 8;
    doReturn(workflowTimeout).when(util).getConfigurationCheckTimeout(any());

    String result = workflow.beginUpgradeInstance(upgradeRequest);
    assertEquals("Upgrade Successful", result);
  }
}

This is the config class I’ve defined:

public class TemporalTestConfig {
  private static final Logger logger = LoggerFactory.getLogger(TemporalTestConfig.class);
  private static TestWorkflowEnvironment testWorkflowEnvironment =
      TestWorkflowEnvironment.newInstance();
  private static TestActivityEnvironment testActivityEnvironment =
      TestActivityEnvironment.newInstance();

  @Bean
  public TestWorkflowEnvironment testWorkflowEnvironment() {
    return testWorkflowEnvironment;
  }

  @Bean
  public WorkflowClient workflowClient() {
    return testWorkflowEnvironment.getWorkflowClient();
  }
}

However, I am getting this exception:
Caused by: java.lang.NullPointerException: Cannot invoke "com.nvidia.ipp.jenkinsupgradeprocess.util.Util.getConfigurationCheckTimeout(String)" because "this.util" is null
even though I have injected the @MockBean Util util. It’s making me think that the wrong Workflow Bean is getting called, but I haven’t been able to get this working.

How can I fix this issue? Thanks in advance.

Unlike activity implementation, you cannot make a workflow impl a Component class as it has a custom life cycle thats not tied to any specific Spring ones. So Autowire would not work in your workflows (again does in activity impls).

What you could do is use ApplicationContextAware interface so for example:

@WorkflowImpl(taskQueues = "TaskQueue")
public class UpgradeInstanceWorkflowImpl implements UpgradeInstanceWorkflow, , ApplicationContextAware {
   private ApplicationContext ctx;

  @Override
  public String beginUpgradeInstance(UpgradeRequest upgradeRequest) {
     Util util = ctx.getBean(Util.class);
     // ...
 } 

  @Override
  public void setApplicationContext(ApplicationContext appContext)
          throws BeansException {
      ctx = appContext;
  }
}

If your Util class is serializable would be better to do it from a SideEffect, so something like

Util util = Workflow.sideEffect(Util.class, () -> ctx.getBean(Util.class));

so it’s done only once even if your workflow code runs multiple times during workflow execution lifecycle (for example gets evicted from your worker cache or you restart workers while its running)

Thanks for the response @tihomir. I have added those changes to my workflow implementation.

I’m getting the error Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.context.ApplicationContext.getBean(java.lang.Class)" because "this.ctx" is null

I’m using the import statements:

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

I guess I should mention that in our app we are creating our own ApplicationContext instance…(working with some legacy code):

  public static void main(String[] args) {
    SpringApplication.run(JenkinsUpgradeProcessApplication.class, args).start();
  }

  @Override
  public void onApplicationEvent(final ApplicationReadyEvent event) {
    logger.info("Starting Application");
    logger.info("Initializing Application Context");
    ApplicationContext applicationContext = ApplicationContext.getInstance();
  }

ApplicationContext.java:

@Data
public class ApplicationContext {

  private ApplicationContext() {}

  private static ApplicationContext instance = new ApplicationContext();

  public static ApplicationContext getInstance() {
    return instance;
  }

Is your solution still possible even though we have our own ApplicationContext?

Is your solution still possible even though we have our own ApplicationContext?

I’m not sure, would need to test and get back to you. Another approach could be to have your workflow invoke an activity or local activity which can Autowire Util bean and return it as result if its serializable, or return the timeout config directly

Got it - after reading a bit it seems like the Local Activity should work for returning the timeout config.

Are there any code samples ? I found this from a stack overflow post, Temporal: When and how to use the local activity? - Stack Overflow. But is ‘Account’ class implementation the same as a regular activity? or is it just a regular class?

Yes, your activity implementation can be invoked as “normal” or local activity depending of which stub you use in workflow code, ActivityStub or LocalActivityStub

1 Like

Thank you Tihomir! Using LocalActivity worked well.