Mar 29, 2020 · ⏱ 2 minutes

🚧 Conquering Randomness with Composable Go Application Design

Over the last few months, I got to write a lot of Go code. In addition to all this code, I wrote a lot of tests. Testing in Go is an absolute breeze, mainly because it's built into the language core and is accompanied by fantastic tooling. If you write a lot of tests, you start to observe that randomness becomes your enemy: IDs will always look different, they aren't predictable.

You try to work around those problems by stopping to use snapshot testing and writing more manual assertions, but the feeling that the experience could be improved sticks around: You've got to do something about it.

Luckily, there's a straightforward change in application design that might help you throughout this: Try to break up functionality like ID generation, the invocation of external services and other workflows that produce unpredictable or network-dependent results into their own domains, pass them from the application entrypoint to your destination, and suddenly everything becomes easier.

You'll be able to swap out ID generation with a mocked version that will be completely deterministic and thus predictable, you can test your software without requiring any external services, you've transformed your application to be completely predictable again, at least for the cases when you need it.

To make this a bit more approachable, let's say we've got an application to order food online. In our request handler, we're creating a new order with a globally-unique identifier we provided.

func CreateOrder(w http.ResponseWriter, r *http.Request) {
orderId := uuid.New()
err := db.Insert(dbOrder{
Id: orderId,
// ...
})
if err != nil {
return "", fmt.Errorf("could not insert order: %w", err)
}
return orderId, nil
}

In this case, we'd be out of luck when trying to snapshot the final output, for example, the order created in the database. However, we can change the top-most part slightly, to pass in a function generating our identifier

func Wrap(generateId func() string) http.HandlerFunc {
return func CreateOrder(w http.ResponseWriter, r *http.Request) {
orderId := generateId()
err := db.Insert(dbOrder{
Id: orderId,
// ...
})
if err != nil {
return "", fmt.Errorf("could not insert order: %w", err)
}
return orderId, nil
}
}

Now, we've wrapped the handler func with a function that will pass in a generateId argument, which is a function that returns an identifier to be used for order creation.

If we now construct the rest of our application as composable as to pass this function around from the entrypoint that we use to run our testing on, we're able to mock this function easily, making the generated identifiers predictable at all times.

The same also goes for the usage of external services: Because we only want to test our own business logic, not the transport layer or even the external service itself, we'd benefit from breaking this functionality up the same way we did with ID generation. We could then build simple test cases around different responses from the mocked service.


I hoped you enjoyed this post and could get some value out of it, maybe you just got an idea on how to build a dependency-free testing solution for your product. If you've got any questions, suggestions, or feedback in general, don't hesitate to reach out on Twitter or by mail.

πŸ„ The latest posts, delivered to your inbox.Subscribe