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
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)
All assertion functions have the same signature:
is.<comparison>(got, want interface{}, message ...any) bool
or, without an expected value, for self-contained checks like is.Nil, is.True, etc:
is.<comparison>(got interface{}, message ...any) bool
All assertion functions return true when the expected value was found and false otherwise.
func TestOne( t *testing.T ) { result, err := doSomething() if !is.Null(err, "something failed badly") { return } is.True(result.OK,"looks good") }
Data-Driven Testing ¶
Data-driven tests are also a much nicer way to create tests which are easier to understand and modify.
go's style for ad-hoc test arrays looks something like this:
tests := []struct{ given string then string }{ {"1 + 2", "3"}, {"2 * 5", "10"}, ... } for _, tc := range tests { ...
The is package, however, provides is.TableFromString(), which converts a very simple text table into a [][]string array:
tests := is.TableFromString(` My calculator should be able to perform simple arithmetic | given | then | | ----- | ---- | | 1 + 2 | 3 | | 2 * 5 | 10 | Any lines which do not begin with a '|' character are ignored `) for _, tc := range tests.DataRows() { // tc = []string{"1 + 2","3"} ... }
Example (DataDrivenTesting) ¶
package main import ( "fmt" "strconv" "strings" "codeberg.org/japh/is" ) var t = &mockTestingT{} func main() { // testCases.DataRows() [][]string returns the data from the table, with // leading and trailing spaces trimmed. // // Notes: // - indenting is ignored // - lines that do not begin with a | are ignored // - lines that only contain the characters |, +, = or - are ignored testCases := is.TableFromString(` An example of using a text table to define a group of tests. +---------+---------+------+-------------------+-------------------------+ | 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? | +---------+---------+------+-------------------+-------------------------+ `) field := testCases.FieldByNameFunc() for line, tc := range testCases.DataRows() { when := field(tc, "when") then, _ := strconv.ParseFloat(field(tc, "then"), 64) errStr := field(tc, "error") comment := field(tc, "comment") name := when if name == "" { name = errStr } // 'given' is a little more complicated: // split into space separated fields // then split each field into a 'name' and 'value' given := map[string]float64{} givenNames := []string{} // need a separate slice to keep keys in order for _, f := range strings.Fields(field(tc, "given")) { nv := strings.Split(f, ":") name := nv[0] value := nv[1] given[name], _ = strconv.ParseFloat(value, 64) givenNames = append(givenNames, name) } t.Run( fmt.Sprintf("line %d: %s", line, name), func(t *mockTestingT /* t *testing.T */) { // a little extra context if comment != "" { t.Logf("# %s", comment) } // given <preconditions> for _, name := range givenNames { value := given[name] t.Logf("set %s = %v", name, value) } // when <action> t.Logf("run %s ...", when) // then <expectations> t.Logf("got %v (error: %v)", then, errStr) }, ) } } //////////////////////////////////////////////////////////////////////////////// // // 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: === RUN line 0: empty expression run ... got 0 (error: empty expression) === RUN line 1: 1 + 2 run 1 + 2 ... got 3 (error: ) === RUN line 2: a + b run a + b ... got 0 (error: a is not a number) === RUN line 3: a + b set a = 1 set b = 2 run a + b ... got 3 (error: ) === RUN line 4: sqrt(a) set a = 1 run sqrt(a) ... got 1 (error: ) === RUN line 5: sqrt(a) set a = 4 run sqrt(a) ... got 2 (error: ) === RUN line 6: sqrt(a) # need imaginary numbers? set a = -1 run sqrt(a) ... got 0 (error: undefined)
Index ¶
- Variables
- func Equal(r reporter, got, want any, message ...any) bool
- func Error(r reporter, got error, message ...any) bool
- func ErrorMatching(r reporter, got error, want string, message ...any) bool
- func Fail(r reporter, message ...any) bool
- func False(r reporter, got bool, message ...any) bool
- func Fatal(r reporter, message ...any)
- func FieldByNameFunc[T any](m *FieldMap) func(row []T, name string) (value T)
- func Logf(r reporter, message ...any)
- func Nil(r reporter, got any, message ...any) bool
- func NotEqual(r reporter, got, want any, message ...any) bool
- func NotError(r reporter, got error, message ...any) bool
- func NotNil(r reporter, got any, message ...any) bool
- func NotZero(r reporter, got any, message ...any) bool
- func Pass(r reporter, message ...any) bool
- func True(r reporter, got bool, message ...any) bool
- func Zero(r reporter, got any, message ...any) bool
- type FieldMap
- type Is
- func (is *Is) Equal(got, want any, message ...any) bool
- func (is *Is) Error(got error, message ...any) bool
- func (is *Is) ErrorMatching(got error, want string, message ...any) bool
- func (is *Is) Fail(message ...any) bool
- func (is *Is) False(got bool, message ...any) bool
- func (is *Is) Fatal(message ...any)
- func (is *Is) Logf(message ...any)
- func (is *Is) MessagePrefix(prefix ...any)
- func (is *Is) New(t reporter) *Is
- func (is *Is) Nil(got any, message ...any) bool
- func (is *Is) NotEqual(got, want any, message ...any) bool
- func (is *Is) NotError(got error, message ...any) bool
- func (is *Is) NotNil(got any, message ...any) bool
- func (is *Is) NotZero(got any, message ...any) bool
- func (is *Is) Pass(message ...any) bool
- func (c *Is) TableFromString(input string) *Table
- func (is *Is) True(got bool, message ...any) bool
- func (is *Is) Zero(got any, message ...any) bool
- type Table
- func (tbl *Table) AllRowLines() []int
- func (tbl *Table) AllRows() [][]string
- func (tbl *Table) DataRowLines() []int
- func (tbl *Table) DataRows() [][]string
- func (tbl *Table) FieldByNameFunc() func(row []string, fieldName string) string
- func (tbl *Table) HeaderRow() []string
- func (tbl *Table) HeaderRowLine() int
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var SelfTest = false
Functions ¶
func Equal ¶ added in v0.2.6
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 testing.Log() and true is returned Otherwise, testing.Error() is used to report the error and false is returned
func Error ¶ added in v0.2.6
Error checks for a non-nil error
To check for a specific Error, use is.Equal() or is.ErrorMatching()
func ErrorMatching ¶ added in v0.2.6
ErrorMatching passes if
the pattern is empty and err is nil or the pattern is not empty and the err matches the pattern
func Fatal ¶ added in v0.2.10
func Fatal(r reporter, message ...any)
Fatal logs a message and terminates testing by calling testing.FailNow()
func FieldByNameFunc ¶ added in v0.2.14
FieldByNameFunc returns a generic lookup function for returning a field from a slice, indexed by name
func Logf ¶ added in v0.2.6
func Logf(r reporter, message ...any)
Logf logs a message via testing.Logf
func Nil ¶ added in v0.2.6
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 testing.Log() and true is returned Otherwise, testing.Error() is used to report the error and false is returned
func NotEqual ¶ added in v0.2.6
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 testing.Log() and true is returned Otherwise, testing.Error() is used to report the error and false is returned
func NotNil ¶ added in v0.2.6
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 testing.Log() and true is returned Otherwise, testing.Error() is used to report the error and false is returned
Types ¶
type FieldMap ¶ added in v0.2.12
type FieldMap struct {
// contains filtered or unexported fields
}
FieldMap provides a mapping of field names to their position in a list.
This is used to identify a field in a row, based on a column name.
func NewFieldMap ¶ added in v0.2.12
NewFieldMap returns a new FieldMap for an ordered list of field names.
func (*FieldMap) Find ¶ added in v0.2.12
Find returns the position of a matching string within the FieldMap.
Multiple attempts may be made to find the most appropriate match.
If a name is duplicated, only the first instance will be returned.
If none of the attempts succeed, ok will be false.
Search criteria, in order:
- exact cache match (each search result is memoised for performance)
- exact match
- integer match (always returns ok if >= 1, assumes an infinitely large set of fields)
- case-insensitive exact match
- case-insensitive prefix match
Example ¶
ExampleFieldMap demonstrates how a FieldMap can be used to identify columns of a PSV table
package main import ( "fmt" expect "codeberg.org/japh/is" ) func main() { fields := []string{"foo", "food", "Footnote", "", "1", "2"} fm := expect.NewFieldMap(fields) testCases := []struct{ name, desc string }{ {"foo", "exact match"}, {"food", "exact match"}, {"foot", "case insensitive prefix"}, {"Foot", "case sensitive prefix"}, {"FO", "first case-insensitive prefix"}, {"Fo", "first case-sensitive prefix"}, {"", "empty name - only matches if an empty field exists"}, // searching fields via numbers {"1", "match numeric name"}, {"2", "match numeric name"}, {"+2", "force field number. +2 does not match field name '2', but is a positive integer"}, {"3", "fictitious field"}, {"7", "fictitious field"}, // numeric boundaries {"0", "ignored - field numbers start at 1"}, {"7", "fictitious field"}, } fmt.Printf("Fields: %q\n", fields) for _, tc := range testCases { pos, ok := fm.Find(tc.name) fmt.Printf("find %-6q => %d %v (%s)\n", tc.name, pos, ok, tc.desc) } }
Output: Fields: ["foo" "food" "Footnote" "" "1" "2"] find "foo" => 0 true (exact match) find "food" => 1 true (exact match) find "foot" => 2 true (case insensitive prefix) find "Foot" => 2 true (case sensitive prefix) find "FO" => 0 true (first case-insensitive prefix) find "Fo" => 2 true (first case-sensitive prefix) find "" => 3 true (empty name - only matches if an empty field exists) find "1" => 4 true (match numeric name) find "2" => 5 true (match numeric name) find "+2" => 1 true (force field number. +2 does not match field name '2', but is a positive integer) find "3" => 2 true (fictitious field) find "7" => 6 true (fictitious field) find "0" => 0 false (ignored - field numbers start at 1) find "7" => 6 true (fictitious field)
type Is ¶
type Is struct {
// contains filtered or unexported fields
}
Is wraps a *testing.T struct for reporting the results of tests.
Typical use:
func TestSomething(t *testing.T) { is := is.New(t) ... is.Equal(got, want, message) }
func New ¶
func New(t reporter) *Is
New creates a new 'is' checker, which can then be used to define assertions in a manner closer to plain english:
is.{expectation}( {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) Error ¶
Error checks for a non-nil error
To check for a specific Error, use is.Equal() or is.ErrorMatching()
func (*Is) ErrorMatching ¶ added in v0.1.2
ErrorMatching passes if
the pattern is empty and err is nil or the pattern is not empty and the err matches the pattern
func (*Is) Fatal ¶ added in v0.2.10
Fatal logs a message and terminates testing by calling t.FailNow()
func (*Is) MessagePrefix ¶ added in v0.2.6
MessagePrefix sets a common prefix for all messages.
The first parameter may be a format, as defined by fmt.Sprintf.
func (*Is) New ¶ added in v0.2.3
Is.New creates a new 'is' checker even if 'is' has already been overridden
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.
e.g.
func TestSomething(t *testing.T) { is := is.New(t) tbl := is.TableFromString(` | °C | message | | ---- | ------------- | | -275 | impossible | | -273 | absolute zero | | 0 | freezing | | 25 | comfortable | | 40 | hot | | 100 | boiling | `) for _, row := tbl.DataRows() { // e.g. row: []string{"-275","impossible"} ... } }
See also description of the Table type.
type Table ¶
type Table struct {
// contains filtered or unexported fields
}
Table represents a 2-dimensional array of strings, intended for test definitions.
Tables of data can be represented as pipe-separated-values (PSV).
Table Parsing Rules
- table rows must begin with a '|' character (after an optional indent)
- columns / cells are separated by a single '|' character and surrounding whitespace
- all rows do not need to have the same number of columns
- the resulting data slices will, however, all have the same length
- a trailing '|' is recommeded per line, but not required
- lines that look like rulers are ignored (e.g. |---|, | === | or +---+)
- completely empty rows are ignored (e.g. | | | |)
- any other lines are ignored
Exampes
tbl := is.TableFromString(` | animal | legs | wings | | ------- | ---- | ----- | | cat | 4 | | | chicken | 2 | 2 | `) for _, row := range tbl.DataRows() { // here we get: // []string{"cat","4",""} // []string{"chicken","2","2"} }
Known Issues ¶
It is not possible to include a '|' character within a cell's data.
is.TableFromString(`| "|" |`) => []string{"\"","\""} is.TableFromString(`| \| |`) => []string{"\\",""}
is.Table is a drop-in-subset-replacement for codeberg.org/japh/psv's Table struct
See codeberg.org/japh/psv if you want to format or sort data etc.
Example (CreatingAndUsingTables) ¶
package main import ( "fmt" "codeberg.org/japh/is" ) func main() { tbl := is.TableFromString(` Tables should be introduced with an explanation. This table has been made a bit more fancy to demonstrate that there may be multiple header row(s). Data starts after the first horizontal ruler that appears after a row with some kind of data. +---------+------+-------+--------+ | animal | legs | wings | weight | | ======= | ==== | ===== | ====== | | cat | 4 | | 3 | | chicken | 2 | 2 | 1 | +---------+------+-------+--------+ `) fmt.Println("All Rows:") for _, row := range tbl.AllRows() { fmt.Printf(" %#v\n", row) } fmt.Println("header Row:") fmt.Printf(" %#v\n", tbl.HeaderRow()) fmt.Println("Data Rows:") for _, row := range tbl.DataRows() { fmt.Printf(" %#v\n", row) } }
Output: All Rows: []string{"animal", "legs", "wings", "weight"} []string{"cat", "4", "", "3"} []string{"chicken", "2", "2", "1"} header Row: []string{"animal", "legs", "wings", "weight"} Data Rows: []string{"cat", "4", "", "3"} []string{"chicken", "2", "2", "1"}
Example (RowLines) ¶
package main import ( "fmt" "strings" "codeberg.org/japh/is" ) func main() { tblStr := ` A demonstration of line numbering +---------+------+-------+--------+ | animal | legs | wings | weight | | ======= | ==== | ===== | ====== | | cat | 4 | | 3 | | chicken | 2 | 2 | 1 | +---------+------+-------+--------+ ` tbl := is.TableFromString(tblStr) fmt.Println("Input:") for l, line := range strings.Split(tblStr, "\n") { fmt.Printf(" %2d:%s\n", l+1, strings.Trim(line, " ")) } fmt.Println("All Rows:") rowLines := tbl.AllRowLines() for r, row := range tbl.AllRows() { fmt.Printf(" %2d: %#v\n", rowLines[r], row) } fmt.Println("Header Row:") fmt.Printf(" %2d: %#v\n", tbl.HeaderRowLine(), tbl.HeaderRow()) fmt.Println("Data Rows:") rowLines = tbl.DataRowLines() for r, row := range tbl.DataRows() { fmt.Printf(" %2d: %#v\n", rowLines[r], row) } }
Output: Input: 1: 2: 3:A demonstration of line numbering 4: 5:+---------+------+-------+--------+ 6:| animal | legs | wings | weight | 7:| ======= | ==== | ===== | ====== | 8:| cat | 4 | | 3 | 9:| chicken | 2 | 2 | 1 | 10:+---------+------+-------+--------+ 11: All Rows: 6: []string{"animal", "legs", "wings", "weight"} 8: []string{"cat", "4", "", "3"} 9: []string{"chicken", "2", "2", "1"} Header Row: 6: []string{"animal", "legs", "wings", "weight"} Data Rows: 8: []string{"cat", "4", "", "3"} 9: []string{"chicken", "2", "2", "1"}
func TableFromString ¶
TableFromString parses a multi-line string representation of a table, and returns a Table object containing a [][]string slice.
This implementation does not return any errors. The assumption is that test tables are always used in a controlled, static, testing environment.
func (*Table) AllRowLines ¶ added in v0.2.0
AllRowLines returns a slice that correlates each row returned by AllRows with a line in the original input string.
e.g.
tbl := is.TableFromString(...) rows := tbl.AllRows() lines := tbl.AllRowLines() for r := range rows { fmt.Printf( "row #%d was found on line #%d\n", r, lines[r] ) }
func (*Table) AllRows ¶ added in v0.2.0
AllRows returns the [][]string of data from the entire table, including an optional header row.
The rows returned are guaranteed to all have the same number of columns.
tbl := is.TableFromString(` | animal | legs | wings | | ------- | ---- | ----- | | cat | 4 | | | chicken | 2 | 2 | `) for _, row := range tbl.AllRows() { // here we get: // []string{"animal","legs","wings"} // []string{"cat","4",""} // []string{"chicken","2","2"} }
func (*Table) DataRowLines ¶ added in v0.2.0
DataRowLines returns a slice of line numbers for the data rows only
func (*Table) DataRows ¶
DataRows returns the [][]string of data from the table, WITHOUT an optional header row.
Example ¶
package main import ( "fmt" "codeberg.org/japh/is" ) func main() { tbl := is.TableFromString(` Just a quick table of people's hobbies: | name | hobby | ---- | ----- | Jane | hiking | Max | knitting `) field := tbl.FieldByNameFunc() for _, row := range tbl.DataRows() { fmt.Printf("%s likes %s\n", field(row, "name"), field(row, "hobby")) } }
Output: Jane likes hiking Max likes knitting
func (*Table) FieldByNameFunc ¶ added in v0.2.11
FieldByNameFunc returns a function which returns a column's value from a row of data, indexed by the column name.
Assumption: - the caller will use the same set of names for each row
func (*Table) HeaderRow ¶ added in v0.2.7
HeaderRow returns the first row of the table.
Example ¶
package main import ( "fmt" "codeberg.org/japh/is" ) func main() { tbl := is.TableFromString(` Just a quick table of people's hobbies: | name | hobby | ---- | ----- | Jane | hiking | Max | knitting `) fmt.Printf("Column Names:\n") for _, col := range tbl.HeaderRow() { fmt.Printf(" %s\n", col) } }
Output: Column Names: name hobby
func (*Table) HeaderRowLine ¶ added in v0.2.7
HeaderRowLine returns the line numbers of the first row in the table.