Testing and mocking - what clicked for me

Rundown on how to transform untestable code to testable, mockable code in Go using interfaces and mocking

Testing and mocking - what clicked for me

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:

  1. how do I select a password that’s not compromised?
  2. 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 the pwchecker, and the mock version of the pwchecker will implement. It has one method called IsPasswordPwnd, so our app code can be sure that it can call that method
  • Handlers is a struct. It’s kind of like an object in PHP land. It has one named property called pwc, which is going to be of type PasswordChecker, the interface. Which means every Handler will have an implementation of that interface under the pwc property.
  • NewHandler is a function that takes a PasswordChecker interface (or implementation), and returns a new Handlers struct where the pwc property is already defined as the first argument to the NewHandler
  • lastly I’ve moved RegisterPost to belong to the Handlers struct, where the h variable refers to the struct itself (kind of like calling $this from within an object method in PHP). Because of this, we know that h.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.

Photo by ShareGrid on Unsplash