A stub is the simplest test pattern that we can use in our tests. Stubs typically implement a particular interface and don't contain any real logic; they just provide fixed answers to calls that are performed through the course of a test.
Let's dissect a short code example that illustrates how we can effectively use the concept of stubs for our tests. The Chapter04/captcha package implements the verification logic behind a CAPTCHA test.
As a big fan of the SOLID principles, I opted to define two interfaces, Challenger and Prompter, to abstract the CAPTCHA image generation and the user-prompting implementation. After all, there is a plethora of different approaches out there for generating CAPTCHA images: we could pick a random image from a fixed set of images, generate them using a neural network, or perhaps even call out to a third-party image generation service. The same could be said about the way we actually prompt our users for an answer. This is how the two interfaces are defined:
// Challenger is implemented by objects that can generate CAPTCHA image
// challenges. type Challenger interface { Challenge() (img image.Image, imgText string) } // Prompter is implemented by objects that display a CAPTCHA image to the // user, ask them to type their contents and return back their response. type Prompter interface { Prompt(img image.Image) string }
At the end of the day, the actual business logic doesn't really care how the CAPTCHA images or the users' answers were obtained. All we need to do is fetch a challenge, prompt the user, and then perform a simple string comparison operation, as follows:
func ChallengeUser(c Challenger, p Prompter) bool { img, expAnswer := c.Challenge() userAnswer := p.Prompt(img) if subtle.ConstantTimeEq(int32(len(expAnswer)), int32(len(userAnswer)))
== 0 { return false } return subtle.ConstantTimeCompare([]byte(userAnswer), []byte(expAnswer)) == 1 }
One interesting, at least in my opinion, aspect of the preceding code is that it uses constant-time string comparisons instead of using the built-in equality operator for comparing the expected answer and the user's response.
Imagine if, in the preceding CAPTCHA scenario we had used a simple string comparison that essentially compares each character and returns false on the first mismatch. Here's how an attacker could slowly brute-force the answer via a timing attack:
- Start by providing answers following the $a pattern and measuring the time it takes to get a response. The $ symbol is a placeholder for all possible alphanumeric characters. In essence, we try combinations such as aa, ba, and so on.
- Once we have identified an operation that takes longer than the rest, we can assume that that particular value of $ (say, 4) is the expected first character of the CAPTCHA answer! The reason this takes longer is that the string comparison code matched the first character and then tried matching the next character instead of immediately returning it, like it would if there was a mismatch.
- Continue the same process of providing answers but this time using the 4$a pattern and keep extending the pattern until the expected CAPTCHA answer can be recovered.
In order to test the ChallengeUser function, we need to create a stub for each of its arguments. This would provide us with complete control over the inputs to the comparison business logic. Here's what the stubs might look like:
type stubChallenger string func (c stubChallenger) Challenge() (image.Image, string) { return image.NewRGBA(image.Rect(0, 0, 100, 100)), string(c) } type stubPrompter string func (p stubPrompter) Prompt(_ image.Image) string { return string(p) }
Pretty simple, right? As you can see, the stubs are devoid of any logic; they just return a canned answer. With the two stubs in place, we can write two test functions that exercise the match/non-match code paths:
func TestChallengeUserSuccess(t *testing.T) { got := captcha.ChallengeUser(stubChallenger("42"), stubPrompter("42")) if got != true { t.Fatal("expected ChallengeUser to return true") } } func TestChallengeUserFail(t *testing.T) { got := captcha.ChallengeUser(stubChallenger("lorem ipsum"), stubPrompter("42")) if got != false { t.Fatal("expected ChallengeUser to return false") } }
Now that we have a general understanding of how stubs work, let's look at another useful test pattern: spies! A spy is nothing more than a stub that keeps a detailed log of all the methods that are invoked on it. For each method invocation, the spy records the arguments that were provided by the caller and makes them available for inspection by the test code.
Surely, when it comes to Go, the most popular spy implementation is the venerable ResponseRecorder type, which is provided by the net/http/httptest package. ResponseRecorder implements the http.ResponseWriter interface and can be used for testing HTTP request handling code without the need to spin up an actual HTTP server. However, HTTP server testing is not that interesting; let's take a look at a slightly more engaging example. The Chapter04/chat package contains a simple chatroom implementation that is perfect for applying the spy test pattern. The following is the definition of the Room type and its constructor:
// Publisher is implemented by objects that can send a message to a user. type Publisher interface { Publish(userID, message string) error } type Room struct { pub Publisher mu sync.RWMutex users []string } // NewRoom creates a new chat root instance that used pub to broadcast
// messages. func NewRoom(pub Publisher) *Room { return &Room{pub: pub} }
As you can see, Room contains a Publisher instance that gets initialized by the value that's passed to the NewRoom constructor. The other interesting public methods that are exposed by the Room type (not shown here but available in this book's GitHub repo) are AddUser and Broadcast. The first method adds new users to the room, while the latter can be used to broadcast a particular message to all the users currently in the room.
Before we write our actual testing code, let's create a spy instance that implements the Publisher interface and records any published messages:
type entry struct { user string message string } type spyPublisher struct { published []entry } func (p *spyPublisher) Publish(user, message string) error { p.published = append(p.published, entry{user: user, message: message}) return nil }
In the preceding spy implementation, each time the Publish method is invoked, the stub will append a {user, message} tuple to the published slice. With our spy ready to be used, writing the actual test is a piece of cake:
func TestChatRoomBroadcast(t *testing.T) { pub := new(spyPublisher) room := chat.NewRoom(pub) room.AddUser("bob") room.AddUser("alice") _ = room.Broadcast("hi") exp := []entry{ {user: "bob", message: "hi"}, {user: "alice", message: "hi"}, } if got := pub.published; !reflect.DeepEqual(got, exp) { t.Fatalf("expected the following messages:\n%#+v\ngot:\n%#+v", exp, got) } }
This test scenario involves creating a new room, adding some users to it, and broadcasting a message to everyone who has joined the room. The test runner's task is to verify that the call to Broadcast did in fact broadcast the message to all the users. We can achieve this by examining the list of messages that have been recorded by our injected spy.