Testing entity workflow patter in Go SDK?

Hello

I’m trying to wrap my head around unit testing workflows and activities.
I’m trying to mimic the “entity workflow pattern” as describe in the typescript sdk: Workflows in TypeScript | Temporal Documentation

Code looks something like this:

...
func (svc *Service) Workflow(ctx workflow.Context, raised_event *event.Event) error {
	if err := workflow.ExecuteActivity(ctx, svc.ActivityOne, raised_event).Get(ctx, &return_value); err != nil {
		return err
	}
	signals := workflow.GetSignalChannel(ctx, config.UPDATE_CHANNEL_NAME)
	selector := workflow.NewSelector(ctx)
	var update_event *event.Event
	selector.AddReceive(signals, func(channel workflow.ReceiveChannel, _ bool) {
		channel.Receive(ctx, &update_event)
	})
	for it := 0; it < config.MAX_ITERATIONS; it++ { 
		selector.Select(ctx)
		workflow.ExecuteActivity(ctx, svc.ActivityTwo, update_event)
		if update_event.IsDone() {
			return nil
		}
		workflow.Sleep(ctx, time.Minute)
	}
	return workflow.NewContinueAsNewError(ctx, svc.Workflow, raised_event)
}

And test:

const day = time.Hour * 24

...

func (s *WorkflowTestSuite) Test_Workflow_Success() {
	raised_event := &event.Event{...}
	in_progress_event := &event.Event{...}
	in_review_event := &event.Event{...}
	done_event := &event.Event{...}
	s.env.RegisterActivity(s.svc.ActivityOne)
	s.env.RegisterActivity(s.svc.ActivityTwo)
	s.env.OnActivity(s.svc.ActivityOne, mock.Anything, raised_event).Return("return value", nil)
	s.env.OnActivity(s.svc.ActivityTwo, mock.Anything, in_progress_event).Return(nil)
	s.env.OnActivity(s.svc.ActivityTwo, mock.Anything, in_review_event).Return(nil)
	s.env.OnActivity(s.svc.ActivityTwo, mock.Anything, done_event).Return(nil)
	s.env.RegisterDelayedCallback(func() {
		s.env.SignalWorkflow(config.UPDATE_CHANNEL_NAME, in_progress_event)
	}, day)
	s.env.RegisterDelayedCallback(func() {
		s.env.SignalWorkflow(config.UPDATE_CHANNEL_NAME, in_review_event)
	}, 2*day)
	s.env.RegisterDelayedCallback(func() {
		s.env.SignalWorkflow(config.UPDATE_CHANNEL_NAME, done_event)
	}, 3*day)
	s.env.ExecuteWorkflow(s.svc.Workflow, raised_event)
	if err := s.env.GetWorkflowError(); err != nil {
		s.Require().True(workflow.IsContinueAsNewError(err))
	}
}
--- FAIL: TestWorkflowTestSuite (0.00s)
    --- FAIL: TestWorkflowTestSuite/Test_Workflow_Success (0.00s)
       ***/pkg/temporal/workflow_testsuite.go:815: PASS:	ActivityOne(string,*event.Event)
        ***/pkg/temporal/workflow_testsuite.go:815: PASS:	ActivityTwo(string,*event.Event)
        ***/pkg/temporal/workflow_testsuite.go:815: PASS:	ActivityTwo(string,*event.Event)
        ***/pkg/temporal/workflow_testsuite.go:815: FAIL:	ActivityTwo(string,*event.Event)
            		at: [workflow_testsuite.go:297 workflow_test.go:65]
        ***/pkg/temporal/workflow_testsuite.go:815: FAIL: 3 out of 4 expectation(s) were met.
            	The code you are testing needs to make 1 more call(s).
            	at: [workflow_testsuite.go:815 workflow_test.go:34 suite.go:137 suite.go:159]

My guess is the workflow manages to iterate through it’s max value and errors out with continue as new?
Any tips on how to make this work?

At first glance, it appears your test is calling ExecuteActivity for ActivityTwo with parameters that weren’t expected in mock. Also you do not need to s.env.RegisterActivity(s.svc.ActivityTwo) since you are mocking it. You may have to debug your code to see why the mock isn’t matching. It could be an oddity with the mocking library too where it isn’t matching the event properly.

Hm, saw now that my obfuscation had misspelled the Activity one function.

The activites are defined as:
func (svc *Service) ActivityOne(ctx context.Context, e *event.Event) (string, error) {}
func (svc *Service) ActivityTwo(ctx context.Context, e *event.Event) error {}

And mocked like this in the unit test:

s.env.OnActivity(s.svc.ActivityOne, mock.Anything, raised_event).Return("return value", nil)
s.env.OnActivity(s.svc.ActivityTwo, mock.Anything, in_progress_event).Return(nil)

The mock.Anything is added since otherwise it can’t match the activity signature without the context parameter.

You may have to debug to see which of the ExecuteActivity calls is not matching the mock. If you can provide a standalone reproduceable case, I can debug and see which one may not be matching. I am unable to tell from the error and code given alone what is failing.

Sure, here is one from the example given above: https://github.com/rgynn/temp_workflow

Would greatly appreciate if you could take a look!

Thank you very much for the reproducer, this definitely helps us debug problems like this.

I believe your problem is you’re calling ExecuteActivity but you are not waiting on the activity to run (or really even start). So sometimes you are actually finishing the workflow before that mock is even called. This is why it can also appear to succeed every once in a while.

If you change:

workflow.ExecuteActivity(ctx, ActivityTwo, update_event)

to:

fut := workflow.ExecuteActivity(ctx, ActivityTwo, update_event)
if err := fut.Get(ctx, nil); err != nil {
  err
}

Then you’ll wait on the activity execution which will ensure it at least starts (and it’s how you can catch errors). If you don’t care whether the activity actually starts, you could put .Maybe() after the end of your mocks to tell the mock system to stop expecting calls.

1 Like

Thank you so much! Yes, that seem to be it.

Not a really intuitive pattern with the future.Get(ctx, nil), but I’ll take it.
I thought future.IsReady did the same thing (waited for the activity to end, but without catching an error).

Have a good rest of the day!

:+1: Unfortunate Go doesn’t have an intuitive future pattern. At least TypeScript would warn you on an unawaited promise, something Go does not for async code (especially not our bespoke futures).