Structuring Tests in Go

How I organize my tests in Go

The Purpose of Tests

1. Don’t use frameworks

func assert(tb testing.TB, condition bool, msg string) 
func ok(tb testing.TB, err error)
func equals(tb testing.TB, exp, act interface{})
func TestSomething(t *testing.T) {
value, err := DoSomething()
if err != nil {
t.Fatalf("DoSomething() failed: %s", err)
}
if value != 100 {
t.Fatalf("expected 100, got: %d", value)
}
}
func TestSomething(t *testing.T) {
value, err := DoSomething()
ok(t, err)
equals(t, 100, value)
}

2. Use the “underscore test” package

package myapptype User struct {
id int
Name string
}
func (u *User) Save() error {
if u.id == 0 {
return u.create()
}
return u.update()

}
func (u *User) create() error { ... }
func (u *User) update() error { ... }
package myapp_testimport (
"testing"
. "github.com/benbjohnson/myapp"
)
func TestUser_Save(t *testing.T) {
u := &User{Name: "Susy Queue"}
ok(t, u.Save())
}

3. Use test-specific types

type DB struct {
*bolt.DB
}
func Open(path string, mode os.FileMode) (*DB, error) {
db, err := bolt.Open(path, mode)
if err != nil {
return nil, err
}
return &DB{db}, nil
}
type TestDB struct {
*DB
}
// NewTestDB returns a TestDB using a temporary path.
func NewTestDB() *TestDB {
// Retrieve a temporary path.
f, err := ioutil.TempFile("", "")
if err != nil {
panic("temp file: %s", err)
}
path := f.Name()
f.Close()
os.Remove(path)
// Open the database.
db, err := Open(path, 0600)
if err != nil {
panic("open: %s", err)
}
// Return wrapped type.
return &TestDB{db}
}
// Close and delete Bolt database.
func (db *TestDB) Close() {
defer os.Remove(db.Path())
db.DB.Close()
}
func TestDB_DoSomething(t *testing.T) {
db := NewTestDB()
defer db.Close()
...
}

4. Use inline interfaces & simple mocks

package yotype Client struct {}// Send sends a "yo" to someone.
func (c *Client) Send(recipient string) error
// Yos retrieves a list of my yo's.
func (c *Client) Yos() ([]*Yo, error)
package myapptype MyApplication struct {
YoClient interface {
Send(string) error
}
}
func (a *MyApplication) Yo(recipient string) error {
return a.YoClient.Send(recipient)
}
package mainfunc main() {
c := yo.NewClient()
a := myapp.MyApplication{}
a.YoClient = c
...
}
package myapp_test// TestYoClient provides mockable implementation of yo.Client.
type TestYoClient struct {
SendFunc func(string) error
}
func (c *TestYoClient) Send(recipient string) error {
return c.SendFunc(recipient)
}
func TestMyApplication_SendYo(t *testing.T) {
c := &TestYoClient{}
a := &MyApplication{YoClient: c}
// Mock our send function to capture the argument.
var recipient string
c.SendFunc = func(s string) error {
recipient = s
return nil
}
// Send the yo and verify the recipient.
err := a.Yo("susy")
ok(t, err)
equals(t, "susy", recipient)
}

Conclusion

Writing databases and distributed systems in Go.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store