Friday 17 May 2024

Recommended Pattern for Shared State in GoDog

Clarity, Concurrency, Convenience

I'm currently making some changes to Cucumber godog which is a behaviour driven development tool for golang. Whilst writing some tests I've strrugled to find a nice pattern for shared state across steps.

What is the recommendation when we have a collection of values that we need to share between steps?

A common anti-pattern I've seen in godog usage is just to have a bunch of global variables. This approach has at least one shortcoming in that this pattern doesn't lend itself to parallel execution because global vars would be shared across all threads and so the threads would trample on each other.

Godog, like Cucumber JVM, does provide a better method to share state across just the steps in a single scenario and this approach isn't succeptible to the issues that globals would. You can see examples of this proposal in the godog documentation where "context.Context" is threaded through the steps and and necessary shared state is comunicated by adding it to the context.

However, the godog docs don't provide evolved examples of the practices around the use of the context and only show storing a single primitive value into the context. 

But, what do I do when I have a collection of state values that I want to share?   

My preferred solution to this is to create a struct that encapsulates all these shared values and which can be added to the context. This is somewhat similar to what we always to in Cucumber JVM.

I then provide a utility function to access the shared state. The pattern I illustrate below is the most convenient I've come up with. This pattern is also convenient in that we can easily trace where the various shared state fields are accessed. 


// Struct to encapsulate the shared state into ordinary typed fields
type SharedState struct {
    Name string
    Url  http.Url
}

// key used to store/retrieve the shared state from the context
var shareStateKey = SharedState{}

// utility function to retreive the state object from the context.
// this utility adds the state object to the context if it's not already present.
// because we're returning a reference to the shared state then this is effectively
// a mutable object, which leads to convenient syntax in the steps below.
func getSharedState(ctx context.Context) (context.Context, *SharedState) {
    v := ctx.Value(shareStateKey)
    if v == nil {
// if shared state wasn't found then create it and update the ctx
        v = &SharedState{
            // empty
        }
        ctx = context.WithValue(ctx, shareStateKey, v)
    }
    return ctx, v.(*SharedState)
}

// example of usage
func someStepDef(ctx context.Context, stepArg string) (context.Context, error) {
    ctx, state := getSharedState(ctx)
    state.Name = stepArg // read/mutate the shared state
    return ctx, nil // we must always return the ctx on all code paths
}

// another example of usage
func someOtherStep(ctx context.Context) (context.Context, error) {
    ctx, state := getSharedState(ctx)
    fmt.Println(state.Name) // read/mutate the shared state
    return ctx, nil // we must always return the ctx on all code paths
}

  

Recommended Pattern for Shared State in GoDog

Clarity, Concurrency, Convenience I'm currently making some changes to  Cucumber godog  which is a behaviour driven development tool for...