Best practices for structuring complex Go workflows

Hi there,

I’m building some workflows to drive the complex lifecycle of entities with lots of state and lots of signals coming from various external services. Unit tests for these workflows are getting real big real quick due to having to set expectations for the various involved Signals and Activities.

Thankfully the lifecycles they implement can be neatly sliced into phases. I was initially thinking of representing these slices as Child Workflows called by the main Workflow, which would provide a convenient abstraction for testing, but the docs advise against it.

My current approach is to implement these phases as methods similar to Workflow methods, and then wrap them in some interface which I can then easily mock.

Main workflow

// This is the mockable bit.
type PhaseRunner interface {
  Execute(workflow.Context, PhaseInput) (PhaseOutput, error)
}

type Base struct {
	phaseRunner PhaseRunner
}

func (base *Base) MyWorkflow(ctx workflow.Context) error {
	output, err := base.phaseRunner.Execute(ctx, nil)
	if err != nil {
		return err
	}
	// do something with "output"
	return nil
}

Phase implementation

func ExecutePhase(ctx workflow.Context, input PhaseInput) (output PhaseOutput, err error) {
  // run activities, handle signals, etc...
  // can be unit-tested like a Workflow
}

type PhaseRunnerFunc func(workflow.Context, PhaseInput) (PhaseOutput, error)

func (f *PhaseRunnerFunc) Execute(ctx workflow.Context, input PhaseInput) (output PhaseOutput, err error) {
  return f(ctx, input)
}

Putting it all together

// Running the real thing.
func run(theWorker worker.Worker) error {
  base := &Base{
    phaseRunner: PhaseRunnerFunc(ExecutePhase),
  }
  theWorker.RegisterWorkflow(base.MyWorkflow)
  return theWorker.Run(worker.InterruptCh())
}

// Unit testing the main workflow.
func (suite *MyWorkflowTestSuite) TestSomething() {
  runner := NewMockPhaseRunner(suite.T())
  // set expectations on runner
  base := &Base {
    phaseRunner: runner,
  }
  suite.env.ExecuteWorkflow(base.MyWorkflow, ...)
}

It’s a bit convoluted but does the job. Does this look reasonable? Any better way to approach this?

Thanks for your help!

This is reasonable, but you should be careful never to use this mechanism to change the implementation of a production workflow as it might break determinism.

1 Like