Golang std testing: Difference between revisions

From wikinotes
 
(28 intermediate revisions by the same user not shown)
Line 13: Line 13:
<blockquote>
<blockquote>
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
go test -run     # run all tests
go test                             # run tests in CWD
go test -run Foo  # run top-level tests containing 'Foo'
go test ./...                        # run all tests
go test -run FooTest                # run top-level tests containing 'FooTest'
go test -run 'FooTest/My TableTest'  # run specific table test
go test -run FooTest ./internal/foo  # run tests matching 'FooTest in package at '/internal/foo'
go test ./internal/foo -v            # run internal/foo tests, and print
</syntaxhighlight>
 
you can also create an ad-hoc test-package and run exclusively a ad-hoc test.go package,<br>
but this is '''not considered the norm'''
<syntaxhighlight lang="bash">
go test internal/foo/bar_test.go
</syntaxhighlight>
</syntaxhighlight>
</blockquote><!-- Usage -->
</blockquote><!-- Usage -->
Line 21: Line 31:
<blockquote>
<blockquote>
The builtin go test framework is fairly minimalist.<br>
The builtin go test framework is fairly minimalist.<br>
Tests are just functions, you can loop them if useful.<br>
Tests are just functions, but you can define subtests with <code>t.Run(...)</code>.<br>
There are no builtin assertions.


Tests are typically kept alongside code.
Tests are typically kept alongside code.
Line 53: Line 64:
= Sample Strategies =
= Sample Strategies =
<blockquote>
<blockquote>
== 1x test per struct ==
== 1:1 function:test with subtests ==
<blockquote>
<blockquote>
The [https://github.com/nanomsg/mangos/tree/3c90520e87f5718a99fc40991345fc4760136dc1 mangos] library uses a really clean approach.<br>
The [https://github.com/nanomsg/mangos/tree/3c90520e87f5718a99fc40991345fc4760136dc1 mangos] library uses a really clean approach.<br>
Line 59: Line 70:


* Instantiates a struct
* Instantiates a struct
* Asserts on all public methods of the struct
* Asserts on all public methods of the struct using <code>t.Run(...)</code>


{| class="wikitable"
{| class="wikitable"
Line 68: Line 79:
|-
|-
|}
|}
</blockquote><!-- 1x test per struct -->
</blockquote><!-- 1:1 function:test with subtests  -->


== Table Tests ==
== Table Tests ==
<blockquote>
<blockquote>
See subtests below.
See subtests below.
<syntaxhighlight lang="go">
func TestHello(t *testing.T) {
    tcases := []struct {                        // <-- slice of structs containing testdata
        test      string
        name      string
        expects    string
    }{
        { test: "Valid Name", name: "Adam", },
        { test: "Nil Name", name: nil, },
        { test: "Invalid Name", name: nil, },
    }
    for _, tcase := range tcases {
        t.Run(tcase.test, func(t *testing.T) {  // <-- t.Run() evaluates each case
            if res := hello.Hello(tcase.name); res != tcase.expects {
                t.Errorf("Failed because...")
            }
        }
    }
}
</syntaxhighlight>
</blockquote><!-- Table Tests -->
</blockquote><!-- Table Tests -->
== Override Function ==
<blockquote>
Go allows you to assign a function to a global variable.<br>
You abstract a real method call with this substitute,<br>
and override the substitute within a test.
Note that this only works if a function is saved to a var
* You cannot override regular functions in tests  (ex. <code>func Foo() {}</code>)
* You cannot override methods in tests (ex. <code>func (this *Foo) Bar() {}</code>)
<syntaxhighlight lang="go">
// foo.go
var removeFile = os.Remove
func DoThing(path string) {
    removeFile(path)
}
</syntaxhighlight>
<syntaxhighlight lang="go">
// foo_test.go
func TestDoThing(t *testing.T) {
    var removedPath string
    removeFile = func(path string) {
        removedPath = path
    }
    DoThing("/var/abc.txt")
    if removedPath != "/var/abc.txt" {
        t.Errorf("Failed because...")
    }
}
</syntaxhighlight>
</blockquote><!--  -->
== Interface/Substitute Concretion ==
<blockquote>
If you're covering a large set of methods throughout your application,<br>
you probably want to abstract it with an interface.
You can define a package with an exported variable with a concretion of the interface in production,<br>
within tests, you can override the variable with a testable stub object.
Filesystem operations are a good example of this (but consider using [[golang afero]] or similar).
{{ expand
| production
|
<syntaxhighlight lang="go">
// internal/filesystem/interface.go
package filesystem
type Interface interface {
    Remove(name string) error
}
</syntaxhighlight>
<syntaxhighlight lang="go">
// internal/filesystem/raw.go
package filesystem
import "os"
type Raw struct{} // implements 'example.com/x/internal/filesystem.Filesystem'
func (this *Raw) Remove(name string) error {
    return os.Remove(name)
}
</syntaxhighlight>
<syntaxhighlight lang="go">
// internal/filesystem/fs.go
package filesystem
var Fs = Raw{} // exported variable, that we can override for entire application within tests
</syntaxhighlight>
<syntaxhighlight lang="go">
// foo.go
package foo
import "example.com/x/internal/filesystem"
func Foo() error {
    return filesystem.Fs.Remove("/var/tmp/foo.txt")
}
</syntaxhighlight>
}}
{{ expand
| testing
|
<syntaxhighlight lang="go">
// internal/filesystem/stub.go
package filesystem
type Stub struct { // implements 'example.com/x/internal/filesystem.Filesystem'
    RemoveError error
}
func (this *Raw) Remove(name string) error {
    return this.RemoveError
}
</syntaxhighlight>
<syntaxhighlight lang="go">
// foo_test.go
package foo
import "example.com/x/internal/filesystem"
func TestMain(t *testing.T) {
    // swap out fs with our stub
    var ExpectedError = errors.New("Expected")
    filesystem.Fs = filesystem.Stub{RemoveError: ExpectedError}
    func t.Run("Raises Error", func(t *testing.T) {
        err := foo.Foo()
        assert.Error(t, err, ExpectedError)
    })
}
</syntaxhighlight>
}}
</blockquote><!-- Interface/Substitute Package -->
</blockquote><!-- Sample Strategies -->
</blockquote><!-- Sample Strategies -->


= Assertions =
= Components =
<blockquote>
== Assertions ==
<blockquote>
<blockquote>
There are no assertions, you are responsible for tests and messages.
There are no assertions, you are responsible for tests and messages.
Line 94: Line 255:
</blockquote><!-- Assertions -->
</blockquote><!-- Assertions -->


= Subtests =
== Subtests ==
<blockquote>
<blockquote>
Evaluate subtests under one function using <code>t.Run("TESTNAME", ...)</code>.
Evaluate subtests under one function using <code>t.Run("TESTNAME", ...)</code>.


These can be expressed as Table Tests:
<syntaxhighlight lang="go">
<syntaxhighlight lang="go">
import "example.com/x/hello"                    // <-- despite sharing package, must import package in test
import "example.com/x/hello"                    // <-- despite sharing package, must import package in test
Line 117: Line 279:
         }
         }
     }
     }
}
</syntaxhighlight>
Or you can define simply unique subtests
<syntaxhighlight lang="go">
import "github.com/stretchr/testify/assert"
import "regexp"
import "testing"
func TestHello(t *testing.T) {
    t.Run("Begins with H", func(t *testing.T) {
        res := hello.Hello("Alex")
        assert.Regexp(t, regexp.MustCompile("^h"), res)
    }
    t.Run("Last word is name", func(t *testing.T) {
        res := hello.Hello("Alex")
        assert.Regexp(t, regexp.MustCompile("Alex$"), res)
    })
}
}
</syntaxhighlight>
</syntaxhighlight>
Line 123: Line 304:
</blockquote><!-- Subtests -->
</blockquote><!-- Subtests -->


= Benchmarking =
== Setup/Teardown ==
<blockquote>
There is no framework-set way of doing this, just run a function.
 
<syntaxhighlight lang="go">
func setup(t *testing.T) {
    t.Log("setup")
}
 
func teardown(t *testing.T) {
    t.Log("teardown")
}
 
func TestHello(t *testing.T) {
    t.Run("Tests that ..", func(t *testing.T) {
        t.setup(t)
        defer t.teardown(t)
        // ...
    }
}
</syntaxhighlight>
</blockquote><!-- Setup/Teardown -->
 
== Benchmarking ==
<blockquote>
<blockquote>
There are tools for benchmarking. See docs
There are tools for benchmarking. See docs
</blockquote><!-- Benchmarking -->
</blockquote><!-- Benchmarking -->


= Fuzzing =
== Logging ==
<blockquote>
Tests have a logger, that you can use to print information during test runs.<br>
(only shown on failing tests)
<syntaxhighlight lang="go">
func TestThing(t *testing.T) {
    t.Log("Show this while testing")
}
</syntaxhighlight>
</blockquote><!-- Logging -->
 
== Fuzzing ==
<blockquote>
<blockquote>
There are tools for fuzzing tests. See docs
There are tools for fuzzing tests. See docs
</blockquote><!-- Fuzzing -->
</blockquote><!-- Fuzzing -->
</blockquote><!-- Components -->

Latest revision as of 22:05, 30 July 2022

Go ships with a minimalist test suite.

Documentation

testing https://pkg.go.dev/testing@go1.18.3

Usage

go test                              # run tests in CWD
go test ./...                        # run all tests
go test -run FooTest                 # run top-level tests containing 'FooTest'
go test -run 'FooTest/My TableTest'  # run specific table test
go test -run FooTest ./internal/foo  # run tests matching 'FooTest in package at '/internal/foo'
go test ./internal/foo -v            # run internal/foo tests, and print

you can also create an ad-hoc test-package and run exclusively a ad-hoc test.go package,
but this is not considered the norm

go test internal/foo/bar_test.go

Example

The builtin go test framework is fairly minimalist.
Tests are just functions, but you can define subtests with t.Run(...).
There are no builtin assertions.

Tests are typically kept alongside code.

// myproject/mypackage/mylib.go

package mypackage

func Hello(name string) string {
    return "Hello, " + name
}
// myproject/mypackage/mylib_test.go

package mypackage

import "testing"

func TestHello(t *testing.T) {
    res := Hello("Adam")
    if res != "Hello, Adam" {
        t.Errorf("Hello() result did not match")
    }
}

Sample Strategies

1:1 function:test with subtests

The mangos library uses a really clean approach.
A library of assertions is created, then each test

  • Instantiates a struct
  • Asserts on all public methods of the struct using t.Run(...)
https://github.com/nanomsg/mangos/blob/master/test/certs_test.go test cases (per struct)
https://github.com/nanomsg/mangos/blob/master/test/util.go test assertions

Table Tests

See subtests below.

func TestHello(t *testing.T) {
    tcases := []struct {                         // <-- slice of structs containing testdata
        test       string
        name       string
        expects    string
    }{
        { test: "Valid Name", name: "Adam", },
        { test: "Nil Name", name: nil, },
        { test: "Invalid Name", name: nil, },
    }

    for _, tcase := range tcases {
        t.Run(tcase.test, func(t *testing.T) {   // <-- t.Run() evaluates each case
            if res := hello.Hello(tcase.name); res != tcase.expects {
                t.Errorf("Failed because...")
            }
        }
    }
}

Override Function

Go allows you to assign a function to a global variable.
You abstract a real method call with this substitute,
and override the substitute within a test.

Note that this only works if a function is saved to a var

  • You cannot override regular functions in tests (ex. func Foo() {})
  • You cannot override methods in tests (ex. func (this *Foo) Bar() {})
// foo.go
var removeFile = os.Remove

func DoThing(path string) {
    removeFile(path)
}
// foo_test.go

func TestDoThing(t *testing.T) {
    var removedPath string
    removeFile = func(path string) {
        removedPath = path
    }

    DoThing("/var/abc.txt")
    if removedPath != "/var/abc.txt" {
        t.Errorf("Failed because...")
    }
}

Interface/Substitute Concretion

If you're covering a large set of methods throughout your application,
you probably want to abstract it with an interface.

You can define a package with an exported variable with a concretion of the interface in production,
within tests, you can override the variable with a testable stub object.

Filesystem operations are a good example of this (but consider using golang afero or similar).

production


// internal/filesystem/interface.go
package filesystem

type Interface interface {
    Remove(name string) error
}
// internal/filesystem/raw.go
package filesystem
import "os"

type Raw struct{} // implements 'example.com/x/internal/filesystem.Filesystem'

func (this *Raw) Remove(name string) error {
    return os.Remove(name)
}
// internal/filesystem/fs.go
package filesystem

var Fs = Raw{} // exported variable, that we can override for entire application within tests
// foo.go
package foo
import "example.com/x/internal/filesystem"

func Foo() error {
    return filesystem.Fs.Remove("/var/tmp/foo.txt")
}


testing


// internal/filesystem/stub.go
package filesystem

type Stub struct { // implements 'example.com/x/internal/filesystem.Filesystem'
    RemoveError error
}

func (this *Raw) Remove(name string) error {
    return this.RemoveError
}
// foo_test.go
package foo
import "example.com/x/internal/filesystem"

func TestMain(t *testing.T) {
    // swap out fs with our stub
    var ExpectedError = errors.New("Expected")
    filesystem.Fs = filesystem.Stub{RemoveError: ExpectedError}

    func t.Run("Raises Error", func(t *testing.T) {
        err := foo.Foo()
        assert.Error(t, err, ExpectedError)
    })
}


Components

Assertions

There are no assertions, you are responsible for tests and messages.

func TestHello(t *testing.T) {
    // log message and fail (but continue executing)
    t.Errorf("expected: %v\nreceived: %v", expects, received)
    t.Fail()         // mark test as failed, but continue
    t.FailNow()      // mark test as failed and stop executing

    t.Skip("Reason") // log, and stop executing

    t.TempDir()      // provides a tempdir that is deleted once test finishes running
}

Subtests

Evaluate subtests under one function using t.Run("TESTNAME", ...).

These can be expressed as Table Tests:

import "example.com/x/hello"                     // <-- despite sharing package, must import package in test

func TestHello(t *testing.T) {
    tcases := []struct {                         // <-- slice of structs containing testdata
        test       string
        name       string
        expects    string
    }{ { test: "ValidName", name: "Adam", },
       { test: "NilName", name: nil, } }

    for _, tcase := range tcases {
        t.Run(tcase.test, func(t *testing.T) {   // <-- t.Run() evaluates each case
            res := hello.Hello(tcase.name)
            if res != tcase.expects {
                t.Errorf("Failed because...")
            }
        }
    }
}

Or you can define simply unique subtests

import "github.com/stretchr/testify/assert"
import "regexp"
import "testing"

func TestHello(t *testing.T) {
    t.Run("Begins with H", func(t *testing.T) {
        res := hello.Hello("Alex")
        assert.Regexp(t, regexp.MustCompile("^h"), res)
    }

    t.Run("Last word is name", func(t *testing.T) {
        res := hello.Hello("Alex")
        assert.Regexp(t, regexp.MustCompile("Alex$"), res)
    })
}

You can also implement setup/teardown this way using defer functions.

Setup/Teardown

There is no framework-set way of doing this, just run a function.

func setup(t *testing.T) {
    t.Log("setup")
}

func teardown(t *testing.T) {
    t.Log("teardown")
}

func TestHello(t *testing.T) {
    t.Run("Tests that ..", func(t *testing.T) {
        t.setup(t)
        defer t.teardown(t)
        // ...
    }
}

Benchmarking

There are tools for benchmarking. See docs

Logging

Tests have a logger, that you can use to print information during test runs.
(only shown on failing tests)

func TestThing(t *testing.T) {
    t.Log("Show this while testing")
}

Fuzzing

There are tools for fuzzing tests. See docs