Testing in Go
Test Composition Patterns, Fixtures & Helpers
Golang Bangalore - Meetup 36
8th Sept 2018
Praveen G Shirali
Golang Bangalore - Meetup 36
8th Sept 2018
Praveen G Shirali
1. Unittests, and other forms of automated tests
2. Testing with Go: Tools, packages, other tools etc
3. Test code organization patterns, with examples
4. Guidelines & Best practices
5. Test fixtures & Helpers
6. Common fixture patterns
1. Pieces of code meant to test other code.
2. Unittests should follow these principles:
Fast --- Milliseconds per test. Second(s) (or less) for all tests Isolated --- Order agnostic. No dependency on state, system, test environments etc Repeatable --- Same results anywhere, anytime, any number of times Self-Validating --- A Test can determine by itself whether it failed or passed. Timely --- Tests are written just before the the code they test
1. Integration, acceptance tests, etc.
2. They have dependencies. The dependencies are real.
3. Tests may involve managing a system state or isolated environments
Setup - Automatically setup the environment and dependencies Teardown - Clean up everything. Restore initial test state
4. Reminder: Its 2018.
5Or simply 'go test' is the tool used to discover and execute tests in Go.
> go help test // description of of what 'go test' does > go help tesfunc // description of the function spec > go test -h // CLI flags and their description > go test // discovers and executes tests in your current package
A function having the following signature:
func TestXxx(*testing.T)
Xxx
- Must begin with a capital letter
TestInvalidLoginReturnsError // valid test name TestarossaFerrari // invalid test name
Test function are discovered from filenames ending with `_test.go`
7
1. Go supports exported
and private
variable identifiers.
2. Test files which belong to a package have access to both exported
and private
code
3. Test files can also exist in a special package with the name <package>_test
.
4. Files from <package>
and <package>_test
can co-exist in the same directory.
5. Code in <package>_test
has access only to exported
code from <package>
.
-- WHY ??
1. Packages should be tested by invoking their exported API.
2. This is what an external package would use to call-in
to the package under test.
3. Tests residing in the internal <package>
can be used to test finer details of the implementation.
4. Benchmarks are suited for use in internal packages as they are aimed at quantifying implementation performance.
Go provides comprehensive set of tools to track code-coverage, benchmark, analyze,
and profile go code. These are not covered in this talk.
Contents of 'samples/ex1/sample_test.go'
import "testing" func TestExample(t *testing.T) { t.Log("Hello World!") }
Running 'go test' results in:
PASS ok github.com/pshirali/testing-in-go/samples/ex1 0.006s
Running 'go test -v' results in verbose output:
=== RUN TestExample --- PASS: TestExample (0.00s) sample_test.go:6: Hello World! PASS ok github.com/pshirali/testing-in-go/samples/ex1 0.006s
Shared by both T and B test types: golang.org/pkg/testing/#TB
Skip
Skip the test from the point where it's called
Log
Log a message. (go test '-v')
Error
Log an error. Marks the test FAIL, but continues execution.
Fatal
- Log a fatal error. - Mark the test FAIL, and stop execution of the current test. - Execute any deferred functions. - Proceed to the next test.
Can be used when a test logic needs to be executed with multiple sets of inputs and corresponding results.
Example: The 'hello world' of table driven tests!
Add an arbitrary number of integers and return their sum.
package adder func AddInt(integers ...int) int { sum := 0 for _, i := range integers { sum += i } return sum }
Iterate over test parameters and feed them into the test logic.
func TestAdderUsingTable(t *testing.T) { cases := []struct { integers []int expected int }{ {[]int{}, 0}, // ------------------------------------------- {[]int{0, 0, 0}, 0}, // TABLE: { Input, Expected } {[]int{-1, -2}, -3}, // One set of test params per test iteration {[]int{1, 2, 3}, 6}, // ------------------------------------------- } for _, c := range cases { t.Logf("-------------------- Adding: %v", c.integers) actual := adder.AddInt(c.integers...) if actual != c.expected { t.Errorf("Sum of %v = %v (Actual). Expected: %v", c.integers, actual, c.expected) } } }
Output of 'go test -v'
=== RUN TestAdderUsingTable --- PASS: TestAdderUsingTable (0.00s) adder_test.go:20: -------------------- Adding: [] adder_test.go:20: -------------------- Adding: [0 0 0] adder_test.go:20: -------------------- Adding: [-1 -2] adder_test.go:20: -------------------- Adding: [1 2 3] PASS ok github.com/pshirali/testing-in-go/samples/ex2 0.007s
Notice the use of t.Errorf, not t.Fatalf
if actual != c.expected { t.Errorf("Sum of %v = %v (Actual). Expected: %v", c.integers, actual, c.expected) }
In order to ensure that we continue to test other paramters, should one of them fail,
't.Errorf' has been used.
Subtests are tests within a test.
Test are named <ParentTest>/<SubTest>
, with slash (/) separating parents from children
Ref: golang.org/pkg/testing/#hdr-Subtests_and_Sub_benchmarks
18func TestAdderUsingSubtests(t *testing.T) { cases := []struct { name string integers []int expected int }{ {"Empty", []int{}, 0}, {"Zero", []int{0, 0, 0}, 0}, {"Negative", []int{-1, -2}, -3}, {"Positive", []int{1, 2, 3}, 6}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { t.Logf("-------------------- Adding: %v", c.integers) actual := adder.AddInt(c.integers...) if actual != c.expected { t.Fatalf("Sum of %v = %v (Actual). Expected: %v", c.integers, actual, c.expected) } }) } }
=== RUN TestAdderUsingSubtests === RUN TestAdderUsingSubtests/Empty === RUN TestAdderUsingSubtests/Zero === RUN TestAdderUsingSubtests/Negative === RUN TestAdderUsingSubtests/Positive --- PASS: TestAdderUsingSubtests (0.00s) --- PASS: TestAdderUsingSubtests/Empty (0.00s) adder_test.go:45: -------------------- Adding: [] --- PASS: TestAdderUsingSubtests/Zero (0.00s) adder_test.go:45: -------------------- Adding: [0 0 0] --- PASS: TestAdderUsingSubtests/Negative (0.00s) adder_test.go:45: -------------------- Adding: [-1 -2] --- PASS: TestAdderUsingSubtests/Positive (0.00s) adder_test.go:45: -------------------- Adding: [1 2 3] PASS ok github.com/pshirali/testing-in-go/samples/ex2 0.007s
Do experiment with simple helper functions before settling on a 3rd-party lib.
The ones below are not perfect, they are minimal (on purpose)
// SkipIf skips the test if the condition is true func SkipIf(t *testing.T, condition bool, args ...interface{}) { if condition { t.Skip(args...) } } // Assert fatally fails the test if a condition is false func Assert(t *testing.T, condition bool, args ...interface{}) { if !condition { t.Fatal(args...) } } // Equal deeply compares two types and fatally fails if they are unequal import "reflect" func Equal(t *testing.T, lhs, rhs interface{}, args ...interface{}) { if !reflect.DeepEqual(lhs, rhs) { t.Fatal(args...) } }
The implementation above is used in code samples in rest of the slides.
21t.Helper() -- Ref: golang.org/src/testing/testing.go?s=24302:24327#L669
t.Parallel() -- Ref: golang.org/src/testing/testing.go?s=25187:25209#L696
t.Parallel() in subtests -- Ref: blog.golang.org/subtests
testdata -- Ref: golang.org/cmd/go/#hdr-Description_of_package_lists
Dirs and files that begin with "." or "_" are ignored by go tool Dirs named "testdata" are ignored
1. A counter has an initial value of 0. 2. Exposes method to increment value. Implicit increment by 1. 3. Exposes method to retrieve current value. 4. Exposes method to reset value to 0.
Interface (for reference)
package counter type Resetter interface{ Reset() } type Incrementer interface{ Increment() } type IntValuer interface{ Value() int }
Goroutine safety not guaranteed
package unsafe_counter type unsafeCounter struct { value int } func (c *unsafeCounter) Reset() { c.value = 0 } func (c *unsafeCounter) Increment() { c.value += 1 } func (c *unsafeCounter) Value() int { return c.value } func NewUnsafeCounter() *unsafeCounter { return &unsafeCounter{} }
func TestCounterIncrementIncreasesValue(t *testing.T) { c := NewUnsafeCounter() for i := 1; i < 3; i++ { c.Increment() Assert(t, c.Value() == i, "At Step:", i, "!=", c.Value()) } } func TestCounterIncrementReset(t *testing.T) { c := NewUnsafeCounter() for i := 0; i < 2; i++ { c.Increment() } c.Reset() Assert(t, c.Value() == 0, "Expected 0 after Reset. Got:", c.Value()) }
Counters are stateful. We need a fresh instance in each test.
c := NewUnsafeCounter()
type safeCounter struct { mu sync.RWMutex uc UnsafeCounter } func (c *safeCounter) Reset() { c.mu.Lock() defer c.mu.Unlock() c.uc.Reset() } func (c *safeCounter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.uc.Increment() } func (c *safeCounter) Value() int { c.mu.RLock() defer c.mu.RUnlock() return c.uc.Value() } func NewSafeCounter() *safeCounter { return &safeCounter{uc: NewUnsafeCounter()} }
(X set of tests) * (Y set of inputs)
In our example:
X = All tests which test the behavior of 'Counter' interface Y = Multiple implementations which satisfy 'Counter' Thus, each implementation must satisfy all tests in X
NewUnsafeCounter() *unsafeCounter NewSafeCounter() *safeCounter
Lets assume:
- We only have access to the constructor function, not the structs - We only get pointers to respective structs - Constructor signatures are (and also assumed to be) different for each implementation - The only 'commonality' is that the respective structs satisfy a common (in this case 'Counter') interface
We can't natively pass the constructor of each implementation to
any common test executor function.
The builder encapsulates the construction of each implementation, and
its dependencies.
It exposes a uniform interface through which new
instances of each implementation can be generated.
type CounterBuilder func() Counter
This could be achieved by a function.
func <Interface>Builder() <Interface> { // instantiate dependencies here // return a new instance here return <Constructor>() // returns a pointer }
func UnsafeCounterBuilder() Counter { return NewUnsafeCounter() // returns *unsafeCounter } func SafeCounterBuilder() Counter { return NewSafeCounter() // returns *safeCounter }
Now both Builders satisfy the signature
func() Counter
type suite struct { builder func() Counter } func Suite(builder func() Counter) *suite { return &suite{builder: builder} }
func (s *suite) TestCounterIncrementIncreasesValue(t *testing.T) { // Added (s *suite) c := s.builder() // <---------------------- new instance built by builder for i := 1; i < 3; i++ { c.Increment() Assert(t, c.Value() == i, "At Step:", i, "!=", c.Value()) } } func (s *suite) TestCounterIncrementReset(t *testing.T) { // Added (s *suite) c := s.builder() // <---------------------- new instance built by builder for i := 0; i < 2; i++ { c.Increment() } c.Reset() Assert(t, c.Value() == 0, "Expected 0 after Reset. Got:", c.Value()) }
Changes to the first two lines
1. Tests are now methods implemented on Suite struct 2. Each test gets a fresh instance of Counter supplied by `s.builder()`
func (s *suite) RunAllTests(t *testing.T) { v := reflect.ValueOf(s) // 1. Reflect on the suite for n := 0; n < v.NumMethod(); n++ { // 2. Iterate through method numbers i := v.Method(n).Interface() // 3. Get the method as interface{} if method, ok := i.(func(*testing.T)); ok { // 4. If it matches test signature methodName := reflect.TypeOf(s).Method(n).Name // 5. Get the method's name if strings.HasPrefix(methodName, "Test") { // 6. If it begins with 'Test' t.Run(methodName, method) // 7. Run that method as a test } } } }
Note:
The method 'RunAllTests' also matches the test signature 'func(*testing.T)' Step 6 exists to exclude 'RunAllTests' and avoid a recursive loop
func TestCounterSuite(t *testing.T) { // Table + Subtest driven tests against the Suite cases := []struct { name string builder func() Counter }{ {"SafeCounter", SafeCounterBuilder}, {"UnsafeCounter", UnsafeCounterBuilder}, } for _, c := range cases { t.Run(c.name, Suite(c.builder).RunAllTests) } }
samples/ex3/test_suite $> go test -v === RUN TestCounterSuite === RUN TestCounterSuite/SafeCounter === RUN TestCounterSuite/SafeCounter/TestCounterIncrementIncreasesValue === RUN TestCounterSuite/SafeCounter/TestCounterIncrementReset === RUN TestCounterSuite/UnsafeCounter === RUN TestCounterSuite/UnsafeCounter/TestCounterIncrementIncreasesValue === RUN TestCounterSuite/UnsafeCounter/TestCounterIncrementReset --- PASS: TestCounterSuite (0.00s) --- PASS: TestCounterSuite/SafeCounter (0.00s) --- PASS: TestCounterSuite/SafeCounter/TestCounterIncrementIncreasesValue (0.00s) --- PASS: TestCounterSuite/SafeCounter/TestCounterIncrementReset (0.00s) --- PASS: TestCounterSuite/UnsafeCounter (0.00s) --- PASS: TestCounterSuite/UnsafeCounter/TestCounterIncrementIncreasesValue (0.00s) --- PASS: TestCounterSuite/UnsafeCounter/TestCounterIncrementReset (0.00s) PASS ok github.com/pshirali/testing-in-go/samples/ex3/test_suite 0.007s
samples/ex3/test_suite $> go test -d @@ TestCounterSuite : 0xc0000a6100 *testing.T ==== RunAllTests : 0xc0000a6200 *testing.T ===== Test1 : 0xc0000a6300 *testing.T \_ Counter : 0xc0000b0080 *safe_counter.safeCounter ===== Test2 : 0xc0000a6400 *testing.T \_ Counter : 0xc0000b00e0 *safe_counter.safeCounter ==== RunAllTests : 0xc0000a6500 *testing.T ===== Test1 : 0xc0000a6600 *testing.T \_ Counter : 0xc000070238 *unsafe_counter.unsafeCounter ===== Test2 : 0xc0000a6700 *testing.T \_ Counter : 0xc000070308 *unsafe_counter.unsafeCounter PASS ok github.com/pshirali/testing-in-go/samples/ex3/test_suite 0.007s
func (s *suite) RunAllTests(t *testing.T) { // // Before and After Suite (defer <After>()) // v := reflect.ValueOf(s) for n := 0; n < v.NumMethod(); n++ { i := v.Method(n).Interface() if method, ok := i.(func(*testing.T)); ok { methodName := reflect.TypeOf(s).Method(n).Name if strings.HasPrefix(methodName, "Test") { // Before Test // (don't defer <AfterTest>() here, inside a loop) t.Run(methodName, method) // After Test } } } }
Use them more for test agnostic checks like timing, log, leak detection etc.
Test code and its dependencies should remain within the test
samples/ex3/test_suite $> go test -w >>> [ RunAllTests -Before- ] >>> // SafeCounter --- [ BeforeTest: TestCounterIncrementIncreasesValue ] --- --- [ AfterTest: TestCounterIncrementIncreasesValue ] --- --- [ BeforeTest: TestCounterIncrementReset ] --- --- [ AfterTest: TestCounterIncrementReset ] --- <<< [ RunAllTests -After- ] <<< >>> [ RunAllTests -Before- ] >>> // UnsafeCounter --- [ BeforeTest: TestCounterIncrementIncreasesValue ] --- --- [ AfterTest: TestCounterIncrementIncreasesValue ] --- --- [ BeforeTest: TestCounterIncrementReset ] --- --- [ AfterTest: TestCounterIncrementReset ] --- <<< [ RunAllTests -After- ] <<< PASS ok github.com/pshirali/testing-in-go/samples/ex3/test_suite 0.007s
1. Ability to define Suite local helper methods
func (s *suite) GenerateTestData() func (s *suite) DoSomethingAwesomeWith(c Counter)
2. Can be designed to accept multi-dimension inputs
Suite(c TestConfig, builder ()func Interface).RunAllTests - TestConfig := LocalFileSystem, InMemoryFileSystemAbstraction, InMemoryDatabase, RealDatabase, etc. - builder := Implementations under test
3. Custom test runner
Suite(..).RunAllTests Suite(..).RunSpecificTests Suite(..).RunPrivateTests \_ RunPrivateTests exposes the runner, but hides the 'testMethods' 1. The suite could be published in a package 2. Consumer cannot modify the tests
The 'Counter' example was contrived and overdone on purpose of this presentation
INGREDIENTS: 2 tests 2 implementations 1 fat 'Counter' interface (which happened to have all methods from the implementations)
The 'Counter' type of suite can work well when:
1. Large X -- Large number of tests to validate one input-set/implementation 2. The tests effectively use all methods of the interface
Interfaces must be small and lean
1. If a Suite requires a fat interface, but clusters within the suite use a subset of interfaces. Problem : THE SUITE IS TOO MIXED. It breaks single-responsibility-principle Solution : Break the suite into smaller suites where: - The interface footprint is smaller - The cluser of tests now effectively use all methods
Reflection to iterate identify Test* methods and run them is not a necessity.
The runner could also invoke 't.Run' multiple times.
PROS of reflection vs a manual list of 't.Run' - Works great for large number of tests - Proof against misspelt test names (strings, not TestFunction names) - Proof against maintenance of the 'Run' list CONS - Overkill
Suites satisfy necessity:
Example: 1. TestCounterSuite tests basic counter functionality 2. TestCounterSuite DID NOT test 'goroutine safety' in SafeCounter Testing the goroutine safety of counters would be a different suite by itself
Interface to Suite is not 1:1:
Example: An implementation which returns numbers from the fibonacci series on 'Increment()' could still satisfy the 'Counter' interface, but fail TestCounterSuite
1. MUST NOT: Share state between tests - Stateful Suite members with testdata - State dependent on order of test execution 2. SHOULD NOT: - Perform excessive Setup or Teardown outside the test function - This should be invoked from within each test function per test. - Copy-paste is not considered (as) bad in testing (but don't overdo) 3. SHOULD NOT: Make the Suite complex any more than it should be. - Core Suite responsiblity: (single responsibility principle) a) Encapsulate a collection of tests b) Provide runner(s) to execute those tests - Extend responsibly - "Test" is the king. "Suite" is the helper.
Handler { | Runner { Middleware1 { | TestFunction { Middleware2 { | Env Setup+ defer Teardown BusinessCode {} | Test Setup+ defer Teardown } | Test Logic } | } } | } ----------------------------+------------------------------------- This is a common pattern | Everything that happens in a test, in application code | remains within the test! ----------------------------+-------------------------------------
+
- Setup and Teardown should be invoked from within the test. - If a test fails, you should only have to look into the code within the failing test
Good
<--------- code under test ----------> \ ^ \ | | Keep this distance minimum. Ideally next hop. | / <------------ test code -------------> /
Try to avoid
<--------- code under test ----------> \ ^ \ calls something else | Affects readability. Increases test code footprint calls something / <------------ test code -------------> / 1. Test code nested through many calls can affect readability 2. Larger test code footprint -> More chances of bugs in test code 3. If distributed across multiple files, then fragmented test code affects readability further. >> Simple vs Easy <<
Some examples which I found interesting (related to subtests & test data)
Ref: golang.org/src/cmd/go/go_test.go
testgoData \__ Use of helper functions and their usage in tests
Ref: golang.org/src/net/http/response_test.go
Data driven
Ref: golang.org/src/cmd/gofmt/gofmt_test.go
Use of golden files
Help you prepare the environment and test data to run your test.
Setup - Stuff you do before the test logic begins Teardown - Stuff you do after a test has PASSED or FAILED. The teardown will 'undo' what Setup did.
When to use it?
1. If you need to read/write to temp files on the filesystem 2. Talk to a database 3. Talk to a server over the network 4. Assemble a complex piece of testdata to test with 5. Test resilience or error handling in failure scenarios etc.
Example: The Builders from the 'Counter' Suite
Everything happens in-memory. No persistent state changes anywhere.
Use-and-throw. Nothing to teardown.
Generalized example:
func BuildSomethingComplex(<args>) <someType> { // // Assemble dependencies, // Generate randomized data, templates, etc // specifically tuned as an input for testing // return <someType> }
A good practice is to assemble a new instance of every ingredient.
Each test gets a fresh copy of incredients.
Lowers the risk of errors due to ingredients having some past state.
1. Teardown (cleanup) functions which can be run anywhere.
2. Can be run both before and after tests.
3. If state is clean, Teardown does nothing.
4. If not, Teardown will clean it up.
5. If an error occurs with Teardown, its a catastrophic failure. Future tests may be invalid.
(if the setup/teardown involves global-scope environment changes)
Ref: golang.org/src/syscall/syscall_linux_test.go
[1] chtmpdir [2] Usage of chtmpdir in: TestFaccessat
func fixture(t *testing.T) func() { // setup if err != nil { t.Fatal(..errorMsg..) } return func() { // teardown } }
Usage:
func TestFunction(t *testing.T) { defer fixture(t)() ^ ^ ^ | | +____ This () is for the returned teardown func() | | | +___________ Fixture does setup and returns a teardown func() | +__________________ Deferred: Hence, guaranteed execution after the ... TestFunction completes execution }
[F4] Return data, resources for the test along with teardown func
func TestFunction(t *testing.T) { resource, teardown := fixture(t) defer teardown() // // test code uses resource // ... }
[F5] Return a struct on which teardown is a method (amongst other fields & methods)
func TestFunction(t *testing.T) { strukt := fixture(t) // returns a struct with extended functionality defer strukt.teardown() // cleanup // // strukt.<fields> and struct.<methods> get used in the test // ... }
func LeakingFixture(t *testing.T) func() { var err error err = Step1() if err != nil { t.Fatalf("Failed Step 1") } err = Step2() if err != nil { t.Fatalf("Failed Step 2") } return func() { ..teardown.. } }
If Step2 fails fatally, and Step1 has made a system-scope
state change, that change leaks. Test and teardown are skipped.
Fixture functions which setup first and return a teardown func() must:
1. Raise t.Fatal against the first state change causing code. 2. No more state change causing code must be part of that fixture. 3. A teardown func() would thus not have to run on t.Fatal as: - The error was caused while making the first state change 4. When the setup does succeed, the teardown concerns itself with reverting the one state change that succeeded.
Tests can stack multiple individual fixtures of this nature:
func TestSomethingInIsolation(t *testing.T) func { defer requireContainer(t)() // idempotent setup & teardown defer requireSystemTestConfig(t)() // idempotent setup & teardown defer requireSwitchToContainer(t)() // idempotent setup & teardown // // subprocess go test cmd to re-run this test inside // a container // }
If you skip the () in defer, then Setup runs after the test!
The code is still valid if you miss the parantheses. So, be vigilant.
defer AyyoFixture(t)() \__ Don't miss this
Alternatives -> Fixture formats [F4] and [F5]
They return values, which ask for an explicit defer call of the teardown func
on the subsequent line. This makes it readable.
Golang Bangalore - Meetup 36
8th Sept 2018
Praveen G Shirali