Composition, Testing and Dependencies in Go
With a lot of languages, it is easy to replace a given function or method call with a mocked version. Dynamically replacing or statically injecting mocked dependencies makes testing code much easier. Personally I’ve found this approach less than ideal in Go, so I’ve been looking for another way to achieve some of the goals of mocking and dependency injection. One such approach is to use function composition which has different trade offs compared to dependency injection. After reading Mocking is a Code Smell I was inspired to try this out in Go. I highly recommend it as an in depth explanation of function composition in this application.
The assumption I’m making for the sake of a simple example, is that there’s no active record style library available. Since the example is centred around DB interactions, the way to implement and write tests for the following functionality would be completely different based on the use of that kind of library.
Part of my goal here is to maintain the public function’s signature. I don’t want the caller to need to know about the function’s dependencies or be affected by any refactoring. This is important when slowly adding unit tests to a code base which doesn’t currently have many tests.
In all cases, an integration or functional test would be required to confirm that the query or update is actually correct. There are multiple possible queries/updates that would achieve the same result so only an integration test is sufficient. For example, the test shouldn’t fail if an additional option is provided.
This is basic straight forward code, using db libraries directly. The code is clear but also highly coupled to its dependencies. There’s no separation of side effects from the logic and no dependency is being injected, so nothing can be mocked. A test instance of the DB is required in order to test this. As a result tests are slower and more prone to fragility, and with this sort of code it will often end up only having tests for the happy path.
One approach to refactoring is to extract private functions. It may be a bit easier to read, but this has not improved ease of testing at all. If the error handling was more extensive, then that could also be partially abstracted in the sub functions.
An interface representing the dependencies could be injected. To test this, we’d need to pass in a mock version of the client but this then locks down the interface to that implementation . This means we still need an integration or functional test to confirm that it actually works correctly, as the code to create the client is not testable.
If the public function merely composes other functions, we can separate side effects from the pure functions. If Go had generics, then the composition itself could be written generically. Even so, the compiler is enforcing enough that it probably isn’t worth the effort to directly test the public function itself.
Adapters change the interface from the existing library to a more useful interface for our specific use case. Extracting the dependencies to functions allows them to easily be tested separately.
Testing the private function is as simple as passing in pure functions or closures. Instead of making the test fit whatever interface your library api has, you create the perfect interface for your purpose, and make your dependencies match that. This can be easily unit tested, because the logic can be isolated from both the side effects and the implementation details. As a result, I find the function dependencies version far more readable than the other versions as well.
Setting up testing for the function dependency version is very straightforward, as no special libraries are required. The client must be stubbed out however.
Testing the adapters is very simple as well, for the same reasons as above.
I find the function composition method of handling dependencies by far the easiest to understand. It makes the tests much simpler to implement too. It is not a method of dependency injection, but rather a different method to achieve some of the same goals of dependency injection.