1. 程式人生 > >Structuring Tests in Go

Structuring Tests in Go

The Purpose of Tests

People argue over testing style, whether to use TDD or BDD, or whether tests are even useful at all. Before I get into how I structure my tests in Go, I should explain how I see tests.

Tests should be 2 things:

  1. Self-contained
  2. Easily reproducible

That’s it. They should be self-contained so that changing one part of your test suite doesn’t drastically affect another part. They should be easily reproducible so that someone doesn’t have to go through multiple steps to get their test suite running the same as mine.

With that explanation, here are some of my own rules for testing in Go.

1. Don’t use frameworks

It seems like everyone has their own testing framework for Go. Some do setup/teardown, some do BDD-style function chaining, & some do a web interface. Don’t use them. Go has a perfectly good testing framework built in. Frameworks are also one more barrier to entry for other developers contributing to your code.

I do find that Go’s testing framework makes test assertions verbose so I add three simple helper functions (code here):

func assert(tb testing.TB, condition bool, msg string) 
func ok(tb testing.TB, err error)
func equals(tb testing.TB, exp, act interface{})

These functions consolidate the typical call/check-error/check-value format of tests into an easier to digest format. So instead of:

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)
}
}

We can simply write:

func TestSomething(t *testing.T) {
value, err := DoSomething()
ok(t, err)
equals(t, 100, value)
}

That’s a lot clearer to me. The functions are also in my package so I don’t have to “go get” any dependencies.

2. Use the “underscore test” package

Go doesn’t let you include multiple packages in one folder except for when you use a separate test package. For example, we can have these two files in one folder:

user.go:

package myapp
type 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 { ... }

user_test.go:

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

In this example, my User type can perform a save but whether it creates or updates the user record in the database is internal to my package and my test shouldn’t have to worry about it.

Using a separate “myapp_test” package means that I can’t access the unexported fields and functions in my package. I find this lets me test as a user of the package so I can see if my exported API is usable and complete. When I test in the same package I tend to muck around with the internals of my package which makes my tests brittle.

UPDATE: Dave Cheney brought up a good point on Twitter about the use of the “dot” import. Since this test is testing the exported API, it doesn’t make sense to pretend to be inside of the package. I like that approach and I’ll use it in the future.

3. Use test-specific types

I talked previously about how I wrap some generic types like sql.DB & sql.Tx with my own application-specific types to provide functions specific to my application. In my tests, I do something similar so that I can have functions specific to my tests.

Most of my applications center around a DB type so let’s use that as an example. Let’s say I have an application specific DB type that wraps a Bolt database:

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
}

In my test, I’ll add a TestDB type that automatically opens my database with a temporary file and then provides a close function that I can use for clean up:

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()
}

Now in our test it’s easy to do setup and teardown in 2 lines:

func TestDB_DoSomething(t *testing.T) {
db := NewTestDB()
defer db.Close()
...
}

We can also add other dependencies to our test type so that everything sets up and tears down together.

4. Use inline interfaces & simple mocks

I’ve gone back and forth on best practices for interfaces. Initially I used them frequently but they made my code more complex. Then I stopped using them altogether and it made it more difficult to mock external dependencies.

The biggest turning point for me was realizing that my caller should create the interface instead of the callee providing an interface. This makes sense because the caller can declare exactly what it needs.

Let’s look at an example. Say we are using a third party client for everyone’s favorite social network: Yo. Our client looks like this:

package yo
type 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)

If my application only cares about sending yo’s then it can declare that with an inline interface:

package myapp
type MyApplication struct {
YoClient interface {
Send(string) error
}
}
func (a *MyApplication) Yo(recipient string) error {
return a.YoClient.Send(recipient)
}

Now in our main.go we can initialize the application and set the client:

package main
func main() {
c := yo.NewClient()
a := myapp.MyApplication{}
a.YoClient = c
...
}

And in our test we can use a mock implementation:

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)
}

This is a contrived example that simply passes the recipient through to the client. However, this approach is helpful for testing more complex workflows and for testing errors returned from external dependencies.

You can also use this approach for mocking the file system by having an “os” interface that has functions such as Create() or Open().

Conclusion

With testing built into the Go toolchain, it’s easy to get started and use. Most of the hurdles I’ve experienced have been stylistic but I find that there are usually simple, idiomatic approaches to Go testing.

Use the built-in constructs for testing and make use of types to clarify test workflow. Test your package’s API instead of its internals by using a separate test package, and finally, use inline interfaces to decouple your application from external dependencies. These are just a few ways that can help make your Go tests more clear and maintainable.

相關推薦

Structuring Tests in Go

The Purpose of TestsPeople argue over testing style, whether to use TDD or BDD, or whether tests are even useful at all. Before I get into how I structure

Table driven tests in Go @ Alex Pliutau's Blog

In practice-go we often use table driven testing to be able to test all function scenarios. For example the FindAnagrams() function returns us

Structuring Applications in Go

OverviewFor me, the hardest part of learning Go was in structuring my application. Prior to Go, I was working on a Rails application and Rails makes you st

理解golang反射(reflection in Go)

golang reflect golang反射 go反射機制 反射(reflection)是指在運行時,動態獲取程序結構信息(元信息)的一種能力,是靜態類型語言都支持的一種特性,如Java, golang等。這裏主要詳細介紹golang reflection相關知識類型與接口(Types and

Context propagation over HTTP in Go

https://medium.com/@rakyll/context-propagation-over-http-in-go-d4540996e9b0 Context propagation over HTTP in Go Go 1.7 introduced a built-in

Yet another tool to mock interfaces in Go

Yet another tool to mock interfaces in Go https://itnext.io/yet-another-tool-to-mock-interfaces-in-go-73de1b02c041 As a powerful tool, uni

Downloading large files in Go

package main import ( "fmt" "github.com/cavaliercoder/grab" "time" ) func download(dst string,urls ...string) { n:=len(urls) re,err:=grab.Get

How I organize my applications in Go[轉]

Overview For me, the hardest part of learning Go was in structuring my application. Prior to Go, I was working on a Rails applicat

Ask HN: How do we discourage extreme aptitude tests in the hiring process?

Throughout my entire development career I've dealt with increasingly extreme aptitude tests that other industries simply don't have. For a recent senior ro

Variadic function in Go

What is a variadic function?As we have seen in a functions lesson, a function is a piece of code dedicated to do a particular job. A function takes one or

How big is an int in Go?

How big is an int in Go?Go has several built-in numeric types, “sets of integer or floating-point values.” Some architecture-independent types are uint8 (8

5 advanced testing techniques in Go · Segment Blog

Go has a robust built-in testing library.  If you write Go, you already know this.  In this post we will discuss a handful of strategies to level up your G

Using Parser Combinators in Go

Using Parser Combinators in GoLet’s parse in Golang!How would you write a parser for the following calculator grammar?Digit := "0" | "1" | "2" | "3" | "4"

This Blogpost Deploys Servers in Go, NodeJS, Ruby, Python, Java, PHP

This Blogpost Deploys Servers in Go, NodeJS, Ruby, Python, Java, PHPRunning the embeded Repl.it code in this post will live-deploy simple http servers for

Tinyterm: A silly terminal emulator written in Go

This post is about Tinyterm, a silly hack that I presented as a lightning talk at last month’s Sydney Go User group 1. You can find the original slide

Bit manipulation in Go

Bit manipulation is important to know when doing data compression, cryptography and optimization. Bitwise operations include AND, NOT, OR, XOR and bit s

String manipulation in Go

Basic Operations Get char array of a String greeting := "Comment allez-vous" greeingCharacterArr := []rune(greeting) Get a char at the specific i

Processing spreadsheet data in Go · Applied Go

This article is also available as a video on the Applied Go YouTube channel: It is the shortened version of a lecture in my upcoming minicourse “

Scheduling In Go

IntroductionThe design and behavior of the Go scheduler allows your multithreaded Go programs to be more efficient and performant. This is thanks to the me

Bounds Check Elimination In Go

IntroductionOne day I was talking to Damian Gryski in Slack about some performance improvements he made to his go-metro package. When I first looked at the