Java SDK - Unit testing activity with Signal method in Temporal

Hi,

I have an activity implementation as below.

ActivityImpl.java

@Component
public class ActivityImpl implements Activity {

  @Autowired
  WorkflowClient workflowClient;

  @Autowired
  SomeService service;

  @Override
  public void initializeActivity(Long id) {
    ServiceDTO serviceDTO = null;

    try {
      serviceDTO = service.doOperation(id);
    } catch (CustomWorkflowException e) {
      log.info("Failed to initiate activity for {} : {}", id, e);
      throw Activity.wrap(e);
    }

    SomeWorkflow workflow = workflowClient.newWorkflowStub(
        SomeWorkflow.class, getWorkflowId());
    workflow.signalActivityInitialized(serviceDTO.getId());
  }

  private String getWorkflowId() {
    return Activity.getExecutionContext().getInfo().getWorkflowId();
  }
}

Basically it has to call a service, do some operation and then signal that its done with its execution.

I am trying to Unit test this activity class. But am getting “workflow” as null and testcase fails with null pointer exception.

ActivityTest.java

public class ActivityTest {

  @InjectMocks
  private ActivityImpl activityImpl;

  @Mock
  private SomeService someService;

  @Mock
  private WorkflowClient client;

  private TestWorkflowEnvironment testEnv;
  private TestActivityEnvironment testActivityEnv;
  private Worker worker;

  @Rule
  public TestWatcher watchman =
      new TestWatcher() {
        @Override
        protected void failed(Throwable e, Description description) {
          if (testEnv != null) {
            System.err.println(testEnv.getDiagnostics());
            testEnv.close();
          }
        }
      };

  @Before
  public void setUp() {
    MockitoAnnotations.openMocks(this);
    UserContextManager.createContext(new User("1", null, null));

    testActivityEnv = TestActivityEnvironment.newInstance();
    testEnv = TestWorkflowEnvironment.newInstance();

    worker = testEnv.newWorker(RUN_QUEUE);
    worker.registerWorkflowImplementationTypes(SomeWorkflowImpl.class);
    client = testEnv.getWorkflowClient();
  }

  @After
  public void tearDown() {
    testActivityEnv.close();
    testEnv.close();
  }

  @Rule
  public TestWorkflowRule testWorkflowRule =
      TestWorkflowRule.newBuilder()
          .setWorkflowTypes(SomeWorkflowImpl.class)
          .setDoNotStart(true)
          .build();

  @Test
  public void testInitializeActivityRun() throws CustomWorkflowException {
 
    ServiceDTO serviceDTO = ServiceDTO.builder().id(1L).build();
    when(someService.doOperation(1L)) .thenReturn(serviceDTO);

    worker.registerActivitiesImplementations(activityImpl);
    testEnv.start();

    WorkflowOptions options =
        WorkflowOptions.newBuilder().setTaskQueue(RUN_QUEUE)
            .setContextPropagators(new UserContextPropagator())
            .setWorkflowId("WFL-1").build();
    SomeWorkflow workflow =
        client.newWorkflowStub(SomeWorkflow.class, options);

    WorkflowExecution response = WorkflowClient.start(workflow::run, 1L);
    System.out.println("workflow exec:" + response);

    testEnv.sleep(Duration.ofSeconds(2));
    WorkflowStub.fromTyped(workflow).getResult(String.class);
    verify(someService.atLeast(1)).doOperation(1L);
    testEnv.shutdown();
  }
}

Now when I run the test case, am getting null pointer exception
java.lang.NullPointerException: Cannot invoke “…SomeWorkflow.signalActivityInitialized(java.lang.Long)” because “workflow” is null.

However, in the getWorkflowId() method am able to get the workflow id from activity context. But when I do workflowClient.newWorkflowStub(SomeWorkflow.class, getWorkflowId()), it returns null.

Could you please let me know if something is wrong here, I want to test the activity class. I tried using TestActivityEnvironment but with no luck.

In your test you could have a @TestConfiguration that produces WorkflowClient from TestWorkflowEnvironment.

Following worked for me:

  1. Activity:
@Component
public class MyActivities implements MyActivitiesInterface {

    @Autowired
    WorkflowClient workflowClient;

    @Override
    public void getAndSignalBack(String input) {
        // dummy sleep
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // signal workflow
        MyWorkflowInterface workflow = workflowClient.newWorkflowStub(MyWorkflowInterface.class,
                Activity.getExecutionContext().getInfo().getWorkflowId());
        workflow.upInput("updatedinput");
    }
}
  1. In test:
@RunWith(SpringRunner.class)
@SpringBootTest
class DemoApplicationTests {
	@Autowired
	TestWorkflowEnvironment testWorkflowEnvironment;

	@Autowired
	MyActivities activities;

	@Test
	void testMyWorkflow() {
		Worker worker = testWorkflowEnvironment.newWorker("testQueue");
		worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
		worker.registerActivitiesImplementations(activities);
		testWorkflowEnvironment.start();

		MyWorkflowInterface workflow =
				testWorkflowEnvironment
						.getWorkflowClient()
						.newWorkflowStub(
								MyWorkflowInterface.class,
								WorkflowOptions.newBuilder().setTaskQueue("testQueue").build());
		String result = workflow.exec("originput");
		assertEquals("updatedinput", result);

		testWorkflowEnvironment.close();
	}

	@TestConfiguration
	static class TestConfig {
		private static TestWorkflowEnvironment testWorkflowEnvironment = TestWorkflowEnvironment.newInstance();
		@Bean
		public TestWorkflowEnvironment testWorkflowEnvironment() {
			return testWorkflowEnvironment;
		}

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

I did run into issues using the junit @Rule and TestWorkflowRule as could not find a clean way to define it alongside @TestConfiguration but you can use TestWorkflowEnvironment directly as shown.
You can move the TestConfiguration to its own class and reuse it if needed.
Hope this helps.

1 Like

Thanks for the inputs, Tihomir.

I was able to get my activity class tested with mocked objects itself.
Earlier, the issue was I was not able to get workflowClient instance from test workflow environment passed to autowired WorkflowClient in my activity class. Hence, it was throwing null pointer exception. For now, I was able to get past that by using reflection.

However, your inputs would help me in writing integration tests.

I have couple of questions reg the unit testing best practices though.
My scenario is: Workflow has 4 activities that have to execute sequentially.

Start Activity 1(local) → Signal completion → invoke Activity 2 (Remote) → signal completion → Activity 3 (Remote) → signal completion → Activity 4 (local) → Done.

Unit Testing the workflow Impl class:

@Test
  public void testRunSuccess()  {
    ActivityOneImpl activity1 = mock(ActivityOneImpl.class);
    ActivityTwoInterface activity2 = mock(ActivityTwoInterface.class, withSettings().withoutAnnotations());
    ActivityThreeInterface activity3 = mock(ActivityThreeInterface.class, withSettings().withoutAnnotations());
    ActivityFourImpl activity4 = mock(ActivityFourImpl.class);

    localWorker.registerActivitiesImplementations(activity1);
    localWorker.registerActivitiesImplementations(activity4);
    remoteWorker1.registerActivitiesImplementations(activity2);
    remoteWorker2.registerActivitiesImplementations(activity3);

    testEnv.start();

    WorkflowOptions options =
        WorkflowOptions.newBuilder().setTaskQueue(RUN_QUEUE).build();
    RunWorkflow workflow =
        client.newWorkflowStub(RunWorkflow.class, options);

    WorkflowExecution response = WorkflowClient.start(workflow::run, 1L);

    //Since am mocking the activities, am signalling them manually
    workflow.signalActivity1Done(99999L, dto);
    workflow.signalActivity2Done("OK");
    workflow.signalActivity3Done("OK");
    workflow.signalReplenishmentRunUpdated();

    testEnv.sleep(Duration.ofSeconds(2));
    WorkflowStub workflowStub = client.newUntypedWorkflowStub(response.getWorkflowId(),
        Optional.of(response.getRunId()), Optional.of("ReplenishmentRunWorkflow"));
    assertEquals("Success", workflowStub.getResult(String.class));
  }

Is this correct way of unit testing the workflow implementation, signalling mocked activities manually?

Unit testing the individual activity classes, for eg Activity1 :

@Test
  public void testActivity1() {
   ServiceDTO serviceDTO = ServiceDTO.builder().id(1L).dto(someDTO).build();
   when(someService.doOperation(1L)) .thenReturn(serviceDTO);

    worker.registerActivitiesImplementations(activity1);
    testEnv.start();

    WorkflowOptions options =
        WorkflowOptions.newBuilder().setTaskQueue(RUN_QUEUE).setWorkflowId("WFL-1").build();
    RunWorkflow workflow = client.newWorkflowStub(RunWorkflow.class, options);

    WorkflowExecution response = WorkflowClient.start(workflow::runReplenishment, 1L);

    testEnv.sleep(Duration.ofSeconds(2));
    verify(someService, times(1)).doOperation(1L);
    testEnv.shutdown();
  }

Here am starting the workflow, invoking my activity class via mocking and then testing if its invoking the operation in service class exactly once. Because if it fails, it would have retried 5 times which is the value I have configured. Thats it, am shutting down. Basically am not mocking any other activities in the workflow. Just testing one single activity. Is this right way of testing? Suppose if I have to test Activity4 basically I just signal completion of first 3 manually and just test this fourth alone.

I think this is fine. Another thing you could do is rather than specifying ActivityOptions inside your workflow code you could specify per-activity type options via WorkflowImplementationOptions, see example here.

This way you could test out different ActivityOptions for the activity you are testing by providing them when you register your workflow with the test worker:


worker.registerWorkflowImplementationTypes(testWorkflowImplOptions, SomeWorkflowImpl.class);