Mocking non-activites contained in a workflow

I have been trying to figure out how to mock non-activites embedded inside a workflow. My idea has been to use the chain of responsibility to stack activities and non-activites inside a workflow, I don’t believe this is part of the problem, but it might help understanding the code snippets below. I believe the problems I run into come from 2 limitations.

  1. You can’t pass functions into a workflow
  2. I can’t seem to mock the workflow context

My code looks like this:
the workflow itself

func UpgradeEmailWorkflowV3(ctx workflow.Context, reservationId string) (bool, error) {
	handler := provideHandler()
	r := createReceiver(ctx, reservationId)

	return handleUpgrade(handler, r)
}

the constructor function providing the first handler

func provideHandler() handler {
	roomUpgradeHandler := &getRoomToUpgradeHandler{} // activity
	createMailHandler := &createUpgradeEmailMessageHandler{} // non-activity
	sendHandler := &sendEmailHandler{} // activity

	roomUpgradeHandler.setNext(createMailHandler).setNext(sendHandler)

	return roomUpgradeHandler
}

the handleUpgrade function, I split this up from the workflow function with the idea this could then be tested with mocks due to not having the limitation of serialisable input

func handleUpgrade(h handler, r *receiver) (bool, error) {
	h.execute(r)

	if r.error != nil {
		return false, r.error
	}

	return r.success, nil
}

and finally one of the 3 handlers, notice this handler requires a workflow.Context

type sendEmailHandler struct {
	baseHandler
}

func (h *sendEmailHandler) execute(r *receiver) {
	if r.error == nil {
                // Mocks.SendEmail mock is only used for development purpose, this might be confusing
                // this is not for testing
		r.error = workflow.ExecuteActivity(*r.ctx, Mocks.SendEmail, r.email).Get(*r.ctx, &r.success)
	}
}

I have omitted the other 2 handler implementations and some more code which I think is not required to understand the problem.

My idea was, because the handleUpgrade can get a handler injected, I can unit test this with a handler coming from a custom constructor function which can use mocked handlers for non activites, in this case the createUpgradeEmailMessageHandler.

But inside the unit test I can not create a workflow.Context to put on the receiver struct required by the handler implementations which capture activities. In the below unit test, it does not work because even when the activities are mocked the tests panic on not having a workflow.Context on the receiver struct.

func (test *TestSuite) TestHandleUpgrade() {
	test.env.OnActivity(Mocks.GetRoomToUpgrade, mock.Anything).Return("Mocked Room", nil)
	test.env.OnActivity(Mocks.SendEmail, mock.Anything).Return(true, nil)

	r := &receiver{
		reservationId: uuid.NewString(),
	}

	handler := mockHandlerConstructorFn()

	handleUpgrade(handler, r)
	// assertions...
}

I can also not run the unit test like below, because I can not inject a test-tailored handler provider to a workflow due to it not being serialisable.

// not able to mock the non-activity handler like this
func (test *TestSuite) TestExecuteUpgradeEmailWorkflow() {
	test.env.OnActivity(Mocks.GetRoomToUpgrade, mock.Anything).Return("Mocked Room", nil)
	test.env.OnActivity(Mocks.SendEmail, mock.Anything).Return(true, nil)

	id := uuid.NewString()

	test.env.ExecuteWorkflow(UpgradeEmailWorkflowV3, id)

	test.True(test.env.IsWorkflowCompleted())
	test.NoError(test.env.GetWorkflowError())
}

I understand how I can mock temporal activities, what I want to accomplish is mocking logic which is not inside a temporal activity, but interacts with activities, inside a temporal workflow.

I can accomplish this setting a handler constructor function as a global, and overwriting this in unit tests, but I don’t like the idea of global variables influencing the workflow. I also tempered with the idea of passing a configuration object into the workflow, which gets passed to a factory to create the handler, but this would introduce a lot of overhead for every workflow.

I hope my problem is clear, and somebody can help me coming to a clean solution wherein non-activites inside a workflow can be mocked. It was a long post, thank you for taking the time to read this.

You can do this by creating test-specific workflow functions and passing them to ExecuteWorkflow.

1 Like

Oh I think that will be a solution indeed. :smile:

Thank you!

Can you give an example? I’m unclear as to whether:

  1. the functions are being passed in as workflow args or…

  2. whether there is/was an option on workflow.execute to pass in specific non-activity mocks.

If it’s 2, I’m using the Typescript SDK but couldn’t find that as an option. But this was the best search result.