Mocking With Go Interfaces
Go's implicit interfaces provide great flexibility when it comes to testing our applications. By satisfying the expected contract we can create mocks easily to be used within our test cases. For me there are two main approaches I personally take to mocking.
Lightweight Mocking
I call this lightweight as it is less involved than the method we will come onto next. It basically involves just creating a new structure which satisfies the interface.
Take the following interface where we create a DepositWithdrawer
which represents a person who is capable of depositing and withdrawing from an account.
type DepositWithdrawer interface {
Deposit(id, amount int) (int, error)
Withdraw(id, amount int) (int, error)
}
If we wanted to create a quick, lightweight mock we could do the following
type MockAccountHolder struct {
ReturnAmount int
ReturnErr error
}
func (m MockAccountHolder) Deposit(_, _ int) (int, error) {
return m.ReturnAmount, m.ReturnErr
}
func (MockAccountHolder) Withdraw(_, _ int) (int, error) {
return m.ReturnAmount, m.ReturnErr
}
We now have a quick mock set up that we can determine the return conditions within and utilise in our tests, usage of the above would look something like this
accountHolder := MockAccountHolder{ReturnAmount: 100, ReturnErr: nil}
err := TransferFunds(1000, accountHolder)
if err != nil {
t.Fatalf(...)
}
For a lot of use cases this lightweight approach to mocking your dependencies will work fine, however in some cases it may not provide enough flexibility for what you are after. For example you would have to create a different account holder for each test condition to alter what it would return, which is what brings us to our “more involved” approach which offers us a bit more flexibility.
The More Involved Approach to Mocking
I spent far to long wondering what to title this part. Despite being more involved I would definitely not call this “heavy weight” mocking since it is still fairly simple to set up. The power of this second approach comes from being able to utilise functional properties which will make more sense when you look at the example below
type MockAccountHolder struct {
DepositFunc func(id, amount int) (int, error)
WithdrawFunc func(id, amount int) (int, error)
}
func (m MockAccountHolder) Deposit(id, amount int) (int, error) {
return m.DepositFunc(id, amount)
}
func (m MockAccountHolder) Withdraw(id, amount int) (int, error) {
return m.WithdrawFunc(id, amount)
}
So with the above setup we can now define functions that satisfy the interface on the fly in our tests giving us more flexibility. So if we were going to write our test it might look something more like this
tt := []struct{
Test string
TransferAmount int
AccountHolder DepositWithdrawer
ExpectedErr string
}{
{
Test: "Ensure transfer amount cannot exceed withdrawal limit",
TransferAmount: 500,
AccountHolder: MockAccountHolder{
DepositFunc: func(_, amount int) (int, error) {
return amount, nil
},
WithdrawFunc: func(_, amount int) (int, error) {
if amount > 250 {
return 0, errors.New("amount is higher than available funds")
}
return 0, nil
},
}
},
}
for _, tc := range tt {
err := TransferFunds(tc.TransferAmount, accountHolder)
if err != tc.ExpectedErr {
t.Fatalf(...)
}
}
And that is pretty much it. If we were to write out a bunch of tests the more involved approach brings us a higher degree in flexibility in controlling the return paths for our mock. I personally enjoy this second approach more and use it more times than I don't for testing but ultimately it all depends on your use case.