In my recent years of working with Go, I’ve noticed a significant understanding barrier for newcomers, with regard to mocking third-party and internal logic when designing tests in Go.
With some strictly Object-Oriented (OO) programming languages, we have access to class hierarchies and inheritance features that allow for overwriting certain logical dependencies associated with any class or program that we wish to test. In Go it’s a bit more vague, because Go doesn’t strictly subscribe to the Object-Oriented paradigm.
The back-bone of Go is composed of Interfaces and Structures. Interfaces allow for grouping supported functionality into logical groups, much like Classes in other languages. Defining what a logical group might do is the job of an Interface. Whereas Structures house the actual implementation of the functionality. Structures describe what a logical group actually does, within the context of its Interface. The caveat is, Go does not support explicitly defining relationships between Interfaces and Structures.
The lack of explicit class inheritance sounds convenient and less clunky than the typical Object-Oriented paradigm, but it does open us up to some potential hiccups. We could hard code the relationships between structures by avoiding using Interfaces and only using Structures, but our code would likely become rigid. Rigid code leads to rigid tests, which can allow bugs to squeeze through the cracks.
By designing our features in a modular way, with appropriate use of Interfaces, we can write Go code that represents relationships between features in broader terms. By obfuscating each feature’s logic from the next, each module becomes responsible for its own input and output rather than the program as a whole. Modular tests can then be expanded as needed to account for new standards of user input and potentially destructive input that the program must anticipate and handle appropriately. Generally, the more complex a program gets, the more modules are needed, third-party or otherwise.
That’s where Mocks come into play.
What are Mocks?
Mocking is method by which we can override specific bits of code with fake logic, in order to test other aspects of our code without worrying about the reliability of the logic that we have faked.
Why do we use Mocks?
We mock in our tests, to prevent having live input/output/etc. flowing through our program, when our test is only concerned with a small portion of our program’s logic.
Mocks are particularly good for omitting actions that would update live external sources, such as a database, a server, or a code repository.
How can we design our programs to support Mocks?
As I described in the big wall of text at the start of the article, Mocks are best supported by a modular or functional design. Either paradigm avoids rigid relationships between pieces of logic in our programs, making our lives much easier.
Our Program
Here’s an example program that pulls recent articles from the DEV API and lists the date they were published as well as the title:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"time"
)
func main() {
// Make the request.
r, err := http.Get("https://dev.to/api/articles?per_page=20")
if err != nil {
panic(err)
}
// Read the bytes from the response.
body, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
panic(err)
}
// Transform into usable JSON list.
decoder := json.NewDecoder(bytes.NewReader(body))
var articles []struct {
PublishedAt time.Time `json:"published_at"`
Title string `json:"title"`
}
err = decoder.Decode(&articles)
if err != nil {
panic(err)
}
// Sort the list, newest to oldest.
sort.Slice(articles, func(i, j int) bool {
return articles[i].PublishedAt.UnixNano() > articles[j].PublishedAt.UnixNano()
})
// Print the list.
for _, article := range articles {
fmt.Printf("%v -- %s\n", article.PublishedAt, article.Title)
}
}
The output we see when running the above is something like:
2020-10-27 17:58:59 +0000 UTC -- The 7 Most Popular DEV Posts from the Past Week
2020-10-27 17:40:25 +0000 UTC -- My Online Portfolio
2020-10-27 16:46:39 +0000 UTC -- Pairing with Community Member Rachael Wright-Munn
2020-10-27 14:37:07 +0000 UTC -- I’m Levi Sharpe, Senior Podcast Producer at DEV/Forem- AMA
2020-10-27 14:12:05 +0000 UTC -- A Sinatra Project
2020-10-27 13:21:46 +0000 UTC -- Projects to build that would get you hired as a beginner.
2020-10-27 13:18:16 +0000 UTC -- Create a Landing page in less than 100 lines (incl. CSS) ��
2020-10-27 13:17:06 +0000 UTC -- Forget pay cut, give me a raise to work remotely
2020-10-27 12:33:29 +0000 UTC -- How to hide API KEY in GitHub repo
2020-10-27 12:17:41 +0000 UTC -- How to Build a Secret Dark Mode Toggle for Your Blog
2020-10-27 11:11:57 +0000 UTC -- Open Hacktoberfest Issues on Scaffolder
2020-10-27 09:28:30 +0000 UTC -- Top 5 Free Awesome React.JS Material-UI Admin Dashboard Templates
2020-10-27 07:19:36 +0000 UTC -- 5 Productivity Tools for Mobile App Developers
2020-10-27 06:07:56 +0000 UTC -- Five things you should never say in a software developer interview
2020-10-27 04:31:29 +0000 UTC -- How to log user activities using the Beacon Web API?
2020-10-27 03:57:36 +0000 UTC -- An Intro to JSX
2020-10-27 02:33:43 +0000 UTC -- How do I work on multiple projects simultaneously without losing my mind
2020-10-26 23:34:37 +0000 UTC -- JavaScript Challenge 6: Convert string to camel case
2020-10-26 21:28:26 +0000 UTC -- Final week of Hacktoberfest!
2020-10-26 20:26:02 +0000 UTC -- React Hooks: Managing State With useState Hook
Adding Complexity
Now this is a fairly simple program that will panic
if it critically fails. So, simply building and running it will give a good indication of the health of the program. However, if we want to achieve some more complicated results, things get more interesting.
We are already sorting the DEV articles by publishing date, in ascending order. Let’s filter the results a bit. See the line starting with /** UPDATE:
:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"time"
)
func main() {
// Make the request.
r, err := http.Get("https://dev.to/api/articles?per_page=20")
if err != nil {
panic(err)
}
// Read the bytes from the response.
body, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
panic(err)
}
// Transform into usable JSON list.
decoder := json.NewDecoder(bytes.NewReader(body))
var articles []struct {
PublishedAt time.Time `json:"published_at"`
Title string `json:"title"`
}
err = decoder.Decode(&articles)
if err != nil {
panic(err)
}
// Sort the list, newest to oldest.
sort.Slice(articles, func(i, j int) bool {
return articles[i].PublishedAt.UnixNano() > articles[j].PublishedAt.UnixNano()
})
// Print the list.
for _, article := range articles {
/** UPDATE: filter our results by only showing posts after 12PM GMT (noon). **/
if article.PublishedAt.Hour() < 13 {
continue
}
fmt.Printf("%v -- %s\n", article.PublishedAt, article.Title)
}
}
Running this again, we should only see articles after 12PM:
2020-10-27 17:58:59 +0000 UTC -- The 7 Most Popular DEV Posts from the Past Week
2020-10-27 17:40:25 +0000 UTC -- My Online Portfolio
2020-10-27 16:46:39 +0000 UTC -- Pairing with Community Member Rachael Wright-Munn
2020-10-27 14:37:07 +0000 UTC -- I’m Levi Sharpe, Senior Podcast Producer at DEV/Forem- AMA
2020-10-27 14:12:05 +0000 UTC -- A Sinatra Project
2020-10-27 13:21:46 +0000 UTC -- Projects to build that would get you hired as a beginner.
2020-10-27 13:18:16 +0000 UTC -- Create a Landing page in less than 100 lines (incl. CSS) ��
2020-10-27 13:17:06 +0000 UTC -- Forget pay cut, give me a raise to work remotely
2020-10-26 23:34:37 +0000 UTC -- JavaScript Challenge 6: Convert string to camel case
2020-10-26 21:28:26 +0000 UTC -- Final week of Hacktoberfest!
2020-10-26 20:26:02 +0000 UTC -- React Hooks: Managing State With useState Hook
Writing A Rigid Test
Viola!
So now what?...Well we want to make sure there aren’t any unseen failure states. So, we should write a test for our new logic. That’s going to require a bit of reorganization. Like so:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"time"
)
type Article struct {
PublishedAt time.Time `json:"published_at"`
Title string `json:"title"`
}
func GetArticles() ([]Article, error) {
articles, err := fetch()
if err != nil {
return nil, err
}
// Filter our results by only showing posts after 12PM GMT (noon).
articles = FilterArticles(articles)
// Sort the list, newest to oldest.
sort.Slice(articles, func(i, j int) bool {
return articles[i].PublishedAt.UnixNano() > articles[j].PublishedAt.UnixNano()
})
return articles, nil
}
func FilterArticles(articles []Article) []Article {
var filtered []Article
for _, article := range articles {
if article.PublishedAt.Hour() < 13 {
continue
}
filtered = append(filtered, article)
}
return filtered
}
func ListArticles() error {
articles, err := fetch()
if err != nil {
return err
}
// Print the list.
for _, article := range articles {
fmt.Printf("%v -- %s\n", article.PublishedAt, article.Title)
}
return nil
}
func fetch() ([]Article, error) {
var articles []Article
// Make the request.
r, err := http.Get("https://dev.to/api/articles?per_page=20")
if err != nil {
return nil, err
}
// Read the bytes from the response.
body, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
return nil, err
}
// Transform into usable JSON list.
decoder := json.NewDecoder(bytes.NewReader(body))
err = decoder.Decode(&articles)
if err != nil {
return nil, err
}
return articles, nil
}
func main() {
err := ListArticles()
if err != nil {
panic(err)
}
}
Now that we have broken our logic out into functions, we can write our first test! Let’s create a test for the FilterArticles
function in a new main_test.go
file:
package main
import (
"strings"
"testing"
"time"
)
var testCases = map[string][]Article{
"general": {
{
PublishedAt: time.Unix(1603817199, 0),
Title: "Pairing with Community Member Rachael Wright-Munn",
},
{
PublishedAt: time.Unix(1603809427, 0),
Title: "I’m Levi Sharpe, Senior Podcast Producer at DEV/Forem- AMA",
},
{
PublishedAt: time.Unix(1603807925, 0),
Title: "A Sinatra Project",
},
{
PublishedAt: time.Unix(1603804906, 0),
Title: "Projects to build that would get you hired as a beginner.",
},
{
PublishedAt: time.Unix(1603804696, 0),
Title: "Create a Landing page in less than 100 lines (incl. CSS) ��",
},
{
PublishedAt: time.Unix(1603755277, 0),
Title: "JavaScript Challenge 6: Convert string to camel case",
},
{
PublishedAt: time.Unix(1603751738, 0),
Title: "Ruby CLI application: scraping, object relationships and single source of truth",
},
{
PublishedAt: time.Unix(1603747706, 0),
Title: "Final week of Hacktoberfest!",
},
{
PublishedAt: time.Unix(1603743962, 0),
Title: "React Hooks: Managing State With useState Hook",
},
{
PublishedAt: time.Unix(1603642241, 0),
Title: "What are your favorite Kotlin resources?",
},
{
PublishedAt: time.Unix(1603470582, 0),
Title: "How to accelerate application performance with smart SQL queries.",
},
},
}
func TestFilterArticles(t *testing.T) {
for testCase, want := range testCases {
t.Run(testCase, func(t *testing.T) {
got, err := GetArticles()
if err != nil {
t.Error("failed to get articles", err)
}
for _, wantArticle := range want {
for _, gotArticle := range got {
if strings.Trim(gotArticle.Title, " ") != strings.Trim(wantArticle.Title, " ") {
continue
}
if gotArticle.PublishedAt.Unix() == wantArticle.PublishedAt.Unix() {
break
}
t.Errorf("article not found: %v", wantArticle)
}
}
})
}
}
So here, we have a test to see if all our previously seen (and filtered) articles are accounted for and that their timestamps and titles are the same. Yay!
Improving Test Stability
However, if we run this test enough times, we’ll find that at some point it will fail. After a few hours or perhaps a day, we’ll likely have a completely different article set. That’s because we are hitting the live DEV API!
We want our tests to be reliable indicators of our code’s health. Whether or not the DEV API is sending correctly formatted data, or whether every article is a duplicate, is of no consequence to us — at least not as far as our unit tests are concerned.
Thankfully, we can use a Mock to fix this! We have yet to introduce an Interface to encapsulate all the functionality we’ve provided to the main function. Let’s add an Interface that is going to describe a struct that will handle all this logic when it’s created:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"time"
)
type Fetcher interface {
Fetch() ([]Article, error)
}
type ArticleFetcher struct{}
type Article struct {
PublishedAt time.Time `json:"published_at"`
Title string `json:"title"`
}
// Compile-time check to ensure that our struct implements the interface.
var _ Fetcher = &ArticleFetcher{}
func (a *ArticleFetcher) Fetch() ([]Article, error) {
var articles []Article
// Make the request.
r, err := http.Get("https://dev.to/api/articles?per_page=20")
if err != nil {
return nil, err
}
// Read the bytes from the response.
body, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
return nil, err
}
// Transform into usable JSON list.
decoder := json.NewDecoder(bytes.NewReader(body))
err = decoder.Decode(&articles)
if err != nil {
return nil, err
}
return articles, nil
}
func GetArticles(f Fetcher) ([]Article, error) {
articles, err := f.Fetch()
if err != nil {
return nil, err
}
// Filter our results by only showing posts after 12PM GMT (noon).
articles = FilterArticles(articles)
// Sort the list, newest to oldest.
sort.Slice(articles, func(i, j int) bool {
return articles[i].PublishedAt.UnixNano() > articles[j].PublishedAt.UnixNano()
})
return articles, nil
}
func FilterArticles(articles []Article) []Article {
var filtered []Article
for _, article := range articles {
if article.PublishedAt.Hour() < 13 {
continue
}
filtered = append(filtered, article)
}
return filtered
}
func ListArticles(f Fetcher) error {
articles, err := GetArticles(f)
if err != nil {
return err
}
// Print the list.
for _, article := range articles {
fmt.Printf("%v -- %s\n", article.PublishedAt, article.Title)
}
return nil
}
func main() {
err := ListArticles(new(ArticleFetcher))
if err != nil {
panic(err)
}
}
Here, we’ve thrown the fetch()
logic into a Structure method and assigned that Structure (ArticleFetcher
) to an Interface (Fetcher
) that we can use to create our Mock.
Likewise, the test will have to change:
package main
import (
"strings"
"testing"
"time"
)
type MockArticleFetcher struct {
want []Article
}
var (
_ Fetcher = &MockArticleFetcher{}
testCases = map[string][]Article{
"general": {
{
PublishedAt: time.Unix(1603817199, 0),
Title: "Pairing with Community Member Rachael Wright-Munn",
},
{
PublishedAt: time.Unix(1603809427, 0),
Title: "I’m Levi Sharpe, Senior Podcast Producer at DEV/Forem- AMA",
},
{
PublishedAt: time.Unix(1603807925, 0),
Title: "A Sinatra Project",
},
{
PublishedAt: time.Unix(1603804906, 0),
Title: "Projects to build that would get you hired as a beginner.",
},
{
PublishedAt: time.Unix(1603804696, 0),
Title: "Create a Landing page in less than 100 lines (incl. CSS) ��",
},
{
PublishedAt: time.Unix(1603755277, 0),
Title: "JavaScript Challenge 6: Convert string to camel case",
},
{
PublishedAt: time.Unix(1603751738, 0),
Title: "Ruby CLI application: scraping, object relationships and single source of truth",
},
{
PublishedAt: time.Unix(1603747706, 0),
Title: "Final week of Hacktoberfest!",
},
{
PublishedAt: time.Unix(1603743962, 0),
Title: "React Hooks: Managing State With useState Hook",
},
{
PublishedAt: time.Unix(1603642241, 0),
Title: "What are your favorite Kotlin resources?",
},
{
PublishedAt: time.Unix(1603470582, 0),
Title: "How to accelerate application performance with smart SQL queries.",
},
},
}
)
func NewMockArticleFetcher(want []Article) *MockArticleFetcher {
return &MockArticleFetcher{want}
}
func (m *MockArticleFetcher) Fetch() ([]Article, error) {
return m.want, nil
}
func TestFilterArticles(t *testing.T) {
for testCase, want := range testCases {
mf := NewMockArticleFetcher(want)
t.Run(testCase, func(t *testing.T) {
got, err := GetArticles(mf)
if err != nil {
t.Error("failed to get articles", err)
}
if len(got) == 0 {
t.Error("got empty article list")
}
for _, wantArticle := range want {
for _, gotArticle := range got {
if strings.Trim(gotArticle.Title, " ") != strings.Trim(wantArticle.Title, " ") {
continue
}
if gotArticle.PublishedAt.Unix() == wantArticle.PublishedAt.Unix() {
break
}
t.Errorf("article not found: %v", wantArticle)
}
}
})
}
}
Now we can ensure that no matter how our third-party connector is functioning, we can always trust that in this version of our program, our test will behave consistently.
Recap
We took our simplistic design and expanded the functionality of our “third-party” feature (requesting data from the live DEV API) to, make testing our current filtering logic reliable, and make testing future features easier as well.
That’s all, thanks for reading!
Credits
Header photo credit: Phil Hearing on Unsplash
Top comments (0)