Using Interfaces and Dependency Injection for Effective Unit Testing in Golang

Himanshu pandey
Tech @ Trell
Published in
6 min readOct 26, 2021

Introduction 🙋‍♂️

I am a fresher (or was at least :p) software developer who joined Trell not very long ago. During the time I spent here, I discovered my interest in software architecture and clean code design. Pursuing that, I started learning about various design patterns and software architectures and their benefits and downsides.

Recently, I had an opportunity to implement some of what I have learned in a service I was working on. And since I am also learning about implementing Unit Tests in Golang, I thought it would be a perfect opportunity to show how we can use some common design patterns to make our code more testable.

The Service 🖥️

The service I worked on is a simple one. It’s called ‘grat-alertsand is used for monitoring and alerting for another internal service.

I won’t go in-depth about what the other service does, but to put it simply, it triggers some events at a specified time. And the thing we are monitoring here is if those events were fired at the correct time or not. If we see a significant lag, we send out an alert.

Getting Our Hands Dirty 👷

We will start to dive into code a bit here.

Before writing any code, it’s good to figure out our domain models first. Lucky for us, there are only two of them here

  • Alerter — Used to send alerts to an external service.
  • Monitor — Used to monitor various components.

As you can see, the interfaces are bare-bones with only one method, nothing fancy here.

Now, let’s see their implementation.

Note: For now, the threshold value of lag is hardcoded in the monitor implementation. But can be changed in the future if needed.

Here you can see a slackAlerter struct that implements the Alerter interface and a readerLagMonitor struct that implements the Monitor Interface.

If you are not familiar with Golang or are confused about how these structs are implementing our interfaces, here is a beginner-friendly video explaining just that. The gist is, attach the methods of the interface you want to implement to your struct, and the struct has implemented the said interface. It’s called Implicit Interfaces. We will talk about it later, remember it.

The readerLagMonitor monitors the lag of the ‘reader’ component of our internal service. We need not go in-depth.

Notice how readerLagMonitor has a property alerter of type Alerter instead of slackAlerter. This is important as we do not want our monitor to directly couple to a specific implementation of Alerter, like in this case, slackAlerter. Relying on the interface would give us the freedom to swap Alerter implementation while testing.

You might have also noticed the repository that is of type readerLagRepository. Well, that is a repository used to get the ‘lag’ that we mentioned earlier. The interface for the said repository is displayed below.

Note: The repository interface is defined in the same file as the readerLagMonitor struct, while the implementation lies in its separate package. This practice is called ‘Dependency Inversion’ and is a core part of SOLID design principles.

Dependency Injection 💉

Now that we have taken care of the interfaces and their implementations, we will dive into dependency injection.

Honestly, Dependency Injection is just a fancy word for passing as a parameter. The idea here is, every resource a method or an object depends on should be provided to it via method parameters instead of creating them inside the said method or object.

For example, our readerLagMonitor needs a few things like an alerter, a repository, and the interval between two heartbeats. Same with slackAlerter, it needs an URL to send alerts to some external service. As you can see below in the code snippet, everything required by their factory method is provided to them via method parameters.

We could have done something like this.

The Wrong Way of Doing It

But this would have tightly coupled our factory method, and by extension of it, our model implementation, to other components like config and repository, and made our codebase less testable. Using dependency injection will help us prevent this.

Testing 🧪

Finally, we come to the main event. We will now see very briefly how doing all this setup made our unit testing so much easier.

For our test, we need to know if an alert is triggered if lag exceeds our threshold. Also, we would like to know how many times did it happen. For example, If the interval between each heartbeat is 10 seconds and the test ran for 30 seconds, the alert should ideally trigger 2–3 times.

Since for our unit tests, we cannot use a database or send alerts to Slack, we would need to mock them locally. Here, the benefit of using interfaces and dependency injection comes in handy. We can create a mock repository and alerter that implements the interfaces defined in our domain model.

And then inject these into our monitor. The monitor, now, instead of calling the database, will use the static lag value in our mockRepository, which can we anything we decide, and instead of sending alerts to Slack, will increment the value of the alertCount variable.

The full test file deployed in production is embedded below

Taking It a Step Further

Now, this is probably fine for most beginners who are just learning like me, but we can take this much further.

Remember about Golang having implicit interfaces?? Well, using these implicit interfaces allow us to do something incredible, that is, create unit tests even if external libraries are involved. Let me explain by an example.

Suppose this is our implementation for GetReaderLAG() method

Here we are using Golang’s built-in SQL package and using *sql.DB as our database client. Well, how will we test this function without involving a database??? Well, here lies the real magic of Golang.

If we create an interface, suppose MyDBClient, with the Query() method inside, we can do something like this.

NOTE: We also need to create an interface for the *sql.Rows returned by Query() method.

The existing SQL package already implements the interfaces we created because of ‘implicit interfaces’ in Golang. And since we have an interface ready, we can mock the actual database client if we want.

The example shown here is a small one. You can do this with literally anything. Golang’s way of interfaces might seem out of place initially, but it’s one of its biggest strengths.

Resources 📰

--

--