Documentation
¶
Overview ¶
Package is simplifies and normalises unit test definitions and results.
Features ¶
'is' provides assert-like functions, so that:
each test can be written in a single line, no need for if/else blocks
passing tests are logged, as well as failing tests.
"nothing failed" and "everything passed" are NOT the same!
failing tests show received and wanted values aligned vertically under each other, making differences easier to identify
Inspriation ¶
I find the default formatting of t.Errorf() and the "%v" fmt-directive impossible to read.
https://www.arp242.net/go-testing-style.html give's a few 'style hints', and this package just takes some of those ideas one step further (while only providing a minimal set of comparitors), specifically (in my order of preference):
- make "what is being tested" clear
- use "got" and "want", in that order
- add useful, vertically aligned information
- use test drive [sic] tests
Getting Started ¶
An example setup might be:
import ( "testing" "codeberg.org/japh/is" ) func TestOne(t *testing.T) { is := is.New(t) greeting := "hello" is.Equal(greeting, "hello", "%q - a pleasant greeting", greeting) is.Equal(greeting, "hi", "%q - an informal greeting", greeting) }
Which should produce the output:
go test -v ./... === RUN TestOne one_test.go:8: pass: "hello" - a pleasant greeting one_test.go:9: fail: "hello" - an informal greeting got: string(hello) want: string(hi) --- FAIL: TestOne (0.00s)
Notes: all assertion functions have the generic interface:
is.<comparison>(got, want interface{}, message ...any) bool
or
is.<comparison>(got interface{}, message ...any) bool
for obvious checks like is.Nil, is.True, etc.
All is functions return a simple bool value, where true means the expected value was foound, false otherwise.
func TestOne( t *testing.T ) { result, err := doSomething() if !is.Null(err, "something failed badly") { return } is.True(result.OK,"looks good") }
Tables ¶
Data-driven tests are also a much nicer way to create tests which are easy to understand, modify, extend etc.
go's style for ad-hoc test array looks something like:
tests := []struct{ given string then string }{ {"1 + 2", "3"}, {"2 * 5", "10"}, ... } for _, tc := range tests { ...
is, however, provides is.TableFromString(), which converts a very simple text table into a 2-dimensional array of strings:
tests, _ := is.TableFromString(` | given | then | | ----- | ---- | | 1 + 2 | 3 | | 2 * 5 | 10 | `) for _, tc := range tests.DataRows()[1:] { // BDD style input given := tc[0] then := tc[1] // testable ints got := mycalc.Evaluate(given) want, _ := strconv.Atoi(then) // tests is.Equal(got, want, "%s => %s", given, then) }
Example ¶
package main import ( "errors" "fmt" "strconv" "strings" "codeberg.org/japh/is" ) var t = &mockTestingT{} func main() { testData, _ := is.TableFromString(` A simple example of using a text table to define a group of tests +-----+---------+---------+------+-------------------+-------------------------+ | wip | given | when | then | error | comment | +=====+=========+=========+======+===================+=========================+ | | | | | empty expression | | | | | 1 + 2 | 3 | | | | | | a + b | | a is not a number | | | | a:1 b:2 | a + b | 3 | | | +-----+---------+---------+------+-------------------+-------------------------+ | | a:1 | sqrt(a) | 1 | | | | ==> | a:4 | sqrt(a) | 2 | | | | ==> | a:-1 | sqrt(a) | | undefined | need imaginary numbers? | `) testCases, err := testCasesFromTable(testData) if err != nil { t.Error(err) return } wipOnly := testCases.wipCount > 0 if wipOnly { t.Logf("Running %d of %d tests", testCases.wipCount, len(testCases.tests)) } for _, testCase := range testCases.tests { if wipOnly && !testCase.wip { continue } t.Run( fmt.Sprintf("line %d: %s", testCase.line, testCase.name), func(t *mockTestingT /* t *testing.T */) { // a little extra context if testCase.comment != "" { t.Logf("# %s", testCase.comment) } // given <preconditions> for name, value := range testCase.given { t.Logf("set %s = %v", name, value) } // when <action> t.Logf("run %s", testCase.when) // then <expectations> t.Logf("got %v (error: %v)", testCase.then, testCase.err) }, ) } if testCases.wipCount > 0 { t.Errorf("skipped %d non-wip tests", len(testCases.tests)-testCases.wipCount) } } //////////////////////////////////////////////////////////////////////////////// // // utiltity code to convert a table ([][]string) into something more practical // type testCaseTable struct { wipCount int tests []*testCase } type testCase struct { line int wip bool name string given map[string]float64 when string then float64 err error comment string } func testCasesFromTable(dataTable *is.Table) (*testCaseTable, error) { tct := &testCaseTable{} rows := dataTable.DataRows() lineOfRow := dataTable.RowLineMap() colMap := map[string]int{} for col, name := range rows[0] { colMap[name] = col } for r, row := range rows[1:] { tc := testCaseFromSlice(lineOfRow[r], row, colMap) tct.tests = append(tct.tests, tc) if tc.wip { tct.wipCount++ } } return tct, nil } func testCaseFromSlice(line int, cell []string, col map[string]int) *testCase { tc := &testCase{} tc.line = line tc.wip = cell[col["wip"]] != "" tc.when = cell[col["when"]] tc.then, _ = strconv.ParseFloat(cell[col["then"]], 64) tc.comment = cell[col["comment"]] tc.name = cell[col["when"]] // 'given' is a little more complicated: // split into space separated fields // then split each field into a 'name' and 'value' tc.given = map[string]float64{} for _, f := range strings.Fields(cell[col["given"]]) { nv := strings.Split(f, ":") tc.given[nv[0]], _ = strconv.ParseFloat(nv[1], 64) } if err := cell[col["error"]]; err != "" { tc.err = errors.New(err) } return tc } //////////////////////////////////////////////////////////////////////////////// // // Mock testing.T for demonstration purposes only // // Normal test functions use their testing.T parameters to report errors. // Since Example() must be defined without parameters, we define a little // mock 't' variable to assume the role of testing.T type mockTestingT struct { indent string } func (t *mockTestingT) Run(name string, testFn func(*mockTestingT)) { fmt.Println("=== RUN", name) t.indent = " " testFn(t) t.indent = "" } func (t *mockTestingT) Error(args ...interface{}) { fmt.Printf("%s%s\n", t.indent, fmt.Sprint(args...)) } func (t *mockTestingT) Errorf(msgFmt string, args ...interface{}) { fmt.Printf("%s%s\n", t.indent, fmt.Sprintf(msgFmt, args...)) } func (t *mockTestingT) Log(args ...interface{}) { fmt.Printf("%s%s\n", t.indent, fmt.Sprint(args...)) } func (t *mockTestingT) Logf(msgFmt string, args ...interface{}) { fmt.Printf("%s%s\n", t.indent, fmt.Sprintf(msgFmt, args...)) }
Output: Running 2 of 7 tests === RUN line 13: sqrt(a) set a = 4 run sqrt(a) got 2 (error: <nil>) === RUN line 14: sqrt(a) # need imaginary numbers? set a = -1 run sqrt(a) got 0 (error: undefined) skipped 5 non-wip tests
Index ¶
- type ErrorReporter
- type Is
- func (c *Is) Equal(got, want any, message ...any) bool
- func (c *Is) Error(err error, message ...any) bool
- func (c *Is) Fail(message ...any)
- func (c *Is) False(got bool, message ...any) bool
- func (c *Is) Nil(got any, message ...any) bool
- func (c *Is) NotEqual(got, want any, message ...any) bool
- func (c *Is) NotError(err error, message ...any) bool
- func (c *Is) NotNil(got any, message ...any) bool
- func (c *Is) NotZero(got any, message ...any) bool
- func (c *Is) TableFromString(input string) (*Table, error)
- func (c *Is) True(got bool, message ...any) bool
- func (c *Is) Zero(got any, message ...any) bool
- type Table
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ErrorReporter ¶
ErrorReporter is a minimal testing.T interface (derived from go's standard testing package).
This is basically dependency inversion, so that we can define our own (mock) testing package(s) as needed.
Wherever you see ErrorReporter mentioned in this package, you should mentally replace it with a testing.T instance.
type Is ¶
type Is struct {
// contains filtered or unexported fields
}
Is wraps a *testing.T struct for reporting the results of tests.
func New ¶
func New(r ErrorReporter) *Is
New creates a new 'is' checker, which can then be used to simply check is.{condition}({got}, {want}, {message}, {param}...)
func (*Is) Equal ¶
Equal compares a value against an expected value
Special care is taken to compare interfaces, functions, pointers to structs etc. properly If the values are the same, the message is logged via t.Log() and true is returned Otherwise, t.Error() is used to report the error and false is returned
func (*Is) Nil ¶
Nil compares a value against nil
Special care is taken to compare interfaces, functions, pointers to structs etc. properly If the value is nil, the message is logged via t.Log() and true is returned Otherwise, t.Error() is used to report the error and false is returned
func (*Is) NotEqual ¶
NotEqual compares a value against a value which is not expected
Special care is taken to compare interfaces, functions, pointers to structs etc. properly If the values are not the same, the message is logged via t.Log() and true is returned Otherwise, t.Error() is used to report the error and false is returned
func (*Is) NotNil ¶
NotNil compares a value against nil
Special care is taken to compare interfaces, functions, pointers to structs etc. properly If the value is not nil, the message is logged via t.Log() and true is returned Otherwise, t.Error() is used to report the error and false is returned
func (*Is) TableFromString ¶
TableFromString parses a pipe-separated-values table into a [][]string slice.
This is only to help with situations like:
func TestSomething(t *testing.T) { is := is.New(T) is.TableFromString(...) // access the 'is' package TableFromString() via the 'is' struct ... }
error is always null - only for compatibility with psv.TableFromString()
type Table ¶
type Table struct {
// contains filtered or unexported fields
}
Table is a drop-in-subset-replacement for codeberg.org/japh/psv's Table struct
In contrast to psv's Table, this table only provides access to the [][]string array of split data cells.
Example ¶
package main import ( "fmt" "codeberg.org/japh/is" ) func main() { tbl, err := is.TableFromString(` Just a quick table of people's hobbies: | name | hobby | ---- | ----- | Jane | hiking | Max | knitting `) if err != nil { fmt.Print(err) return } rows := tbl.DataRows() fmt.Printf("Got a table with %d rows and the columns:\n", len(rows)) if len(rows) > 0 { for _, col := range rows[0] { fmt.Printf(" %s\n", col) } // fmt.Printf("\nPretty-Printed Table:\n%s\n", tbl) } }
Output: Got a table with 3 rows and the columns: name hobby
func TableFromString ¶
TableFromString parses a multi-line string representation of a table, and returns an object containing a [][]string array.
This is a minimal, dependency free, drop-in replacement for my more extensive codeberg.org/japh/psv package. If you need more features, you can use that instead.
Table parsing rules:
- only parse lines that begin with a | (after an optional indent)
- columns/cells are separated by a single '|' character
- a trailing '|' is recommeded per line, but not required
- lines that look like rulers are ignored (e.g. |---|, | === | or +---+)
- any other lines are ignored
Known Issues:
- it is not possible to include a | in a cell's data. - e.g. | "|" | => | " | " | => two cells containing '"' instead of 1 cell containing '|' | \| | => | \ | | => '\' and ” instead of '|'
error is always null - only for compatibility with psv.TableFromString()
func (*Table) DataRows ¶
DataRows returns the [][]string of data in the table. The rows returned are guaranteed to all have the same number of columns
tbl := psv.TableFromString(...).DataRows() for _, row := range tbl { for _, cell := range row { ... } } }