Testing and mocking - what clicked for me
Rundown on how to transform untestable code to testable, mockable code in Go using interfaces and mocking
I’m probably not alone in knowing that testing software is (should be) an incredibly important part of your development, but the exact minutiae of how to actually go about it is still somewhat shrouded in a fog.
Sure, all testing libraries describe situations where you’re testing something similar:
package main
func Sum(x int, y int) int {
return x + y
}
func main() {
Sum(5, 5)
}
And then the test for this is equally complicated:
package main
import "testing"
func TestSum(t *testing.T) {
total := Sum(5, 5)
if total != 10 {
t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10)
}
}
Great!
Except my apps make network calls, manipulate json, import libraries, and so on, and for a very very long time I couldn’t move on from these simple (“does 5 + 5 equal 10?”) scenarios to actually testing what my app is (should be) doing.
Don’t even ask me about acceptance testing.
Epiphany
Over the past few days I’ve been chatting with a bunch of folks on the Post Status slack and my brother, plus I’ve been reading two great articles (How to stub requests to remote hosts with Go and Testing API Clients in Go). Collectively they helped me realize the following:
Sometimes it’s not you. Sometimes the code really is untestable, and you have to organize code in a way it becomes testable.
In my specific case, I’m building a Go app that uses the haveibeenpwnd.com API to check whether the password the user gave me on registration has been exposed or not. I’m using https://github.com/masonj88/pwchecker/ for an easy way to check that, so I don’t have to write the API call again.
That module can return 3 different responses:
- API call succeeded, password is not compromised
- API call succeeded, password is compromised
- API call did not succeed due to a network error (rate limiting, DNS went away, whatever)
What I wanted to do is test all these three eventualities, but came up to two very hard questions to answer:
- how do I select a password that’s not compromised?
- how do I even simulate a network failure?
Mocking
Until now I had very hazy ideas about what mocking actually is. Did not understand the need, did not understand where it fit into testing. Working through this specific problem clarified it, so here’s my best attempt at explaining to future me, and hopefully you’ll find it useful as well:
The starting point is that my app directly uses the pwchecker
module, which means it’s coupled. The two cannot be separated, and whatever tests I would need to run would always use the actual module which would make actual network requests. Because of this, I can not test this part of my app without hitting the API.
Testing my app should happen without hitting the actual API. Moreover, I don’t even need to test the module itself, because that’s not my responsibility. The module has its own tests. What I need to test is what my app does in response to what the module returns based on the API calls.
Which means I don’t even need the module itself, I just need a way to give the same possible responses as the module. In this case:
- a response that says password is not compromised, network call successful
- a response that says password is compromised, network call successful
- a response where the network call did not succeed
Which also means I can just create a mock copy of the pwchecker
module that returns one of the three responses based on some specific input I give it. Then the idea is that when we’re in testing mode, my app would be using this mock copy of the module instead of the actual module, and then I could reliably trigger all the possible responses the live module could return.
However in order to do that, I need to be able to tell my app which copy of the module to use. Which brings us back to the first point: if the code is not set up correctly, I can’t tell my app which module to use, and therefore I can’t test it.
Interfaces
I need to make the code modular (pun intended, I’m no coward!), so that I can tell it to use the live pwchecker
on production, and our mocked copy during testing.
That means instead of using module directly, I need to move it behind something I control, that can be initialized.
This is what it was like before (simplified):
package main
func main() {
e := echo.New()
e.POST("/register", RegisterPost)
}
func RegisterPost(c echo.Context) (err error) {
u := new(User)
if err = c.Bind(u); err != nil {
return fmt.Errorf("binding user failed")
}
pwdCheck, err := pwchecker.CheckForPwnage(u.PasswordOne)
...
}
pwchecker.CheckForPwnage
is the direct usage of the module. Can not test it.
This is what it has become:
type PasswordChecker interface {
IsPasswordPwnd(string) (bool, error)
}
type Handlers struct {
pwc PasswordChecker
}
func NewHandler(pwc PasswordChecker) Handlers {
return Handlers{pwc}
}
func (h *Handlers) RegisterPost(c echo.Context) (err error) {
u := new(User)
if err = c.Bind(u); err != nil {
return fmt.Errorf("binding user failed")
}
pwdCheck, err := h.pwc.IsPasswordPwnd(u.PasswordOne)
...
}
Slightly more complicated, but here’s what all of these things are:
PasswordChecker interface
is a generic interface that both the live version of thepwchecker
, and the mock version of thepwchecker
will implement. It has one method calledIsPasswordPwnd
, so our app code can be sure that it can call that methodHandlers
is a struct. It’s kind of like an object in PHP land. It has one named property calledpwc
, which is going to be of typePasswordChecker
, the interface. Which means every Handler will have an implementation of that interface under thepwc
property.NewHandler
is a function that takes aPasswordChecker
interface (or implementation), and returns a newHandlers
struct where thepwc
property is already defined as the first argument to theNewHandler
- lastly I’ve moved
RegisterPost
to belong to theHandlers
struct, where theh
variable refers to the struct itself (kind of like calling$this
from within an object method in PHP). Because of this, we know thath.pwc.IsPasswordPwnd
a function that takes a string, and returns a boolean and an error (or nil on either)
Okay, how to actually use this as we’re not done yet!
First we need to create both the live, and the mock implementations of the PasswordChecker
interface, and pass either of them to the NewHandler
function.
// This is the live
type PwChecker struct{}
func (pw PwChecker) IsPasswordPwnd(password string) (bool, error) {
pwd, err := pwchecker.CheckForPwnage(password)
if err != nil {
return false, err
}
return pwd.Pwnd, nil
}
// This is the mock
type MockPasswordChecker struct{}
func (mpwc MockPasswordChecker) IsPasswordPwnd(password string) (bool, error) {
switch password {
case "NetworkError":
return false, fmt.Errorf("HTTP request failed with error: %s", "Unavailable")
case "FoundPassword":
return true, nil
default:
return false, nil
}
}
Question: How do I know that the PwChecker struct implements the PasswordChecker interface? There’s nothing on PwChecker that refers back to the interface...
Good question. In short: any struct that implements all methods specified by an interface, the struct will automatically and without question implement that interface.
If a struct implements all methods that are in 4 different interfaces, the struct will implement all 4 interfaces.
Back to our structs. This was the old code where we were using the handler:
package main
func main() {
e := echo.New()
e.POST("/register", RegisterPost)
}
// Same code as further up the page, just a copy paste so it's in one place
func RegisterPost(c echo.Context) (err error) {
u := new(User)
if err = c.Bind(u); err != nil {
return fmt.Errorf("binding user failed")
}
pwdCheck, err := pwchecker.CheckForPwnage(u.PasswordOne)
...
}
And this is the new, improved, testable code:
package main
type PasswordChecker interface {
IsPasswordPwnd(string) (bool, error)
}
type Handlers struct {
pwc PasswordChecker
}
func NewHandler(pwc PasswordChecker) Handlers {
return Handlers{pwc}
}
func (h *Handlers) RegisterPost(c echo.Context) (err error) {
u := new(User)
if err = c.Bind(u); err != nil {
return fmt.Errorf("binding user failed")
}
pwdCheck, err := h.pwc.IsPasswordPwnd(u.PasswordOne)
...
}
type PwChecker struct{}
func (pw PwChecker) IsPasswordPwnd(password string) (bool, error) {
pwd, err := pwchecker.CheckForPwnage(password)
if err != nil {
return false, err
}
return pwd.Pwnd, nil
}
func main() {
e := echo.New()
pwc := PwChecker{} // get the struct with the live api
h := NewHandler(pwc) // pass that to the function to get a Handlers to use the live api
e.POST("/register", h.RegisterPost) // RegisterPost will query the live API
}
And for testing, this is what needs to happen:
package main
var (
mockNetworkErrorUser = `{"email":"test@example.com","name":"John Doe","password1":"NetworkError", "password2":"NetworkError"}`
mockNetworkErrorUserReturn = `{"error":"HTTP request failed with error: Unavailable"}`
)
type MockPasswordChecker struct{}
func (mpwc MockPasswordChecker) IsPasswordPwnd(password string) (bool, error) {
switch password {
case "NetworkError":
return false, fmt.Errorf("HTTP request failed with error: %s", "Unavailable")
case "FoundPassword":
return true, nil
default:
return false, nil
}
}
func TestRegisterPostNetworkError(t *testing.T) {
e := echo.New()
mpwc := MockPasswordChecker{}
h := NewHandler(mpwc)
req := httptest.NewRequest(http.MethodPost, "/register", strings.NewReader(mockNetworkErrorUser))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/register")
if assert.NoError(t, h.RegisterPost(c)) {
assert.Equal(t, http.StatusBadGateway, rec.Code)
assert.Equal(t, mockNetworkErrorUserReturn, rec.Body.String())
}
}
Because the testing code is still under the same package main
, we don’t need to include a lot of the code around the interface and functions and methods.
This particular test will simulate what would happen if the library returned with a network error.
Possible problems
Because while testing we aren’t actually using the live module we need to make sure that the mock copy stays in sync with the live module.
What happens if the module changes the return types or changes the number of different things it can return, and we don’t notice for a while?
A possible solution to this would be if package authors would not only create the package, and the tests, but also a mock object we could import just as easily as the actual module.
Conclusion
Interfaces and dependency injection are nice (even if that’s not exactly what’s happening here). Being able to configure how something behaves and not being locked in to one specific implementation is superbly powerful.
I know this was probably one of the more difficult to digest blog posts of mine. Let me know if something doesn’t make sense here, or if I can improve it in any way.