testx

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2021 License: MIT Imports: 15 Imported by: 0

README

testx

testx testx/check testx/checkconv

Go Reference Go Report Card Latest version
CircleCI Codecov

testx is a Go testing library that provides test runners to write reliable and expressive unit tests effortlessly, with minimal boilerplate.

Table of contents

Installation

go get -u github.com/drykit-go/testx

Runners

testx provides 3 types of runners:

  • ValueRunner runs tests on a single value.
  • HTTPHandlerRunner runs tests on http handlers and middlewares.
  • TableRunner runs a series of test cases on a single function.

ValueRunner

ValueRunner runs tests on a single value.

func TestGet42(t *testing.T) {
    testx.Value(Get42()).
        Exp(42).                       // expect 42
        Not(3, "hello").               // expect not 3 nor "hello"
        Pass(checkconv.AssertMany(     // expect to pass input checkers:
            check.Int.InRange(41, 43), //     expect in range [41:43]
            // ...
        )...).
        Run(t)
}

Related examples:

HTTPHandlerRunner

HTTPHandlerRunner runs tests on http handlers and middlewares. It provides methods to perform checks:

  • on the input request (e.g. to ensure it has been attached an expected context by some middleware)
  • on the written response (status code, body, header...)
  • on the execution time.
func TestHandleGetMovieByID(t *testing.T) {
    r, _ := http.NewRequest("GET", "/movies/42", nil)
    // Note: WithRequest can be omitted if the input request is not relevant.
    // In that case it defaults to http.NewRequest("GET", "/", nil).
    testx.HTTPHandlerFunc(HandleGetMovieByID).WithRequest(r).
        Response(
            check.HTTPResponse.StatusCode(check.Int.InRange(200, 299)),
            check.HTTPResponse.Body(check.Bytes.Contains([]byte(`"id":42`))),
        ).
        Duration(check.Duration.Under(10 * time.Millisecond)).
        Run(t)
}

Related examples:

TableRunner

TableRunner runs a series of test cases on a single function.

For monadic functions (1 parameter, 1 return value), its usage is straightforward:

func isEven(n int) { return n&1 == 0 }

func TestIsEven(t *testing.T) {
    testx.Table(isEven).Cases([]testx.Case{
        {In: 0, Exp: true},
        {In: 1, Exp: false},
        {In: -1, Exp: false},
        {In: -2, Exp: true},
    }).Run(t)
}

Note that TableRunner supports any function type (any parameters number, any return values numbers). If the tested function is non-monadic, it requires an additional configuration to know where to inject Case.In and which return value to compare Case.Exp with (see examples below)

Related examples:

Running tests

All runners expose two methods to run the tests: Run and DryRun.

Method Run

Run(t *testing.T) runs the tests, fails t if any check fails, and outputs the results like standard tests:

--- FAIL: TestMyHandler (0.00s)
  /my-repo/myhandler_test.go:64: response code:
      exp 401
      got 200
FAIL
FAIL	my-repo	0.247s
FAIL

Method DryRun

DryRun() runs the tests, store the results and returns a Resulter interface to access the stored results:

// Resulter provides methods to read test results after a dry run.
type Resulter interface {
    // Checks returns a slice of CheckResults listing the runned checks
    Checks() []CheckResult
    // Passed returns true if all checks passed.
    Passed() bool
    // Failed returns true if one check or more failed.
    Failed() bool
    // NChecks returns the number of checks.
    NChecks() int
    // NPassed returns the number of checks that passed.
    NPassed() int
    // NFailed returns the number of checks that failed.
    NFailed() int
}

Related examples:

Further documentation

  • Go package documentation

  • Package check 📄 Readme

    Package check provides extensible and customizable checkers to perform checks on typed values.

  • Package checkconv 📄 Readme

    Package checkconv provides conversion utilities to convert any typed checker to a check.ValueChecker

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var ExpNil expNiler = expNilerImpl{}

ExpNil is a value indicating that nil is an expected value. It is meant to be used as a Case.Exp value in a TableRunner test.

Functions

This section is empty.

Types

type Args

type Args []interface{}

Args is an alias to []interface{}.

func (Args) String

func (args Args) String() string

type Case

type Case struct {
	// Lab is the label of the current case to be printed if the current
	// case fails.
	Lab string

	// In is the input value injected in the tested func.
	In interface{}

	// Exp is the value expected to be returned when calling the tested func.
	// If Case.Exp == nil (zero value), no check is added. This is a necessary
	// behavior if one wants to use Case.Pass or Case.Not but not Case.Exp.
	// To specifically check for a nil value, use ExpNil.
	//
	// 	testx.Table(myFunc, nil).Cases([]testx.Case{
	// 		{In: 123, Pass: checkers},    // Exp == nil, no Exp check added
	// 		{In: 123},                    // Exp == nil, no Exp check added
	// 		{In: 123, Exp: nil},          // Exp == nil, no Exp check added
	// 		{In: 123, Exp: testx.ExpNil}, // Exp == ExpNil, expect nil value
	// 	})
	Exp interface{}

	// Not is a slice of values expected not to be returned by the tested func.
	Not []interface{}

	// Pass is a slice of check.ValueChecker that the return value of the
	// tested func is expected to pass.
	Pass []check.ValueChecker
}

Case represents a Table test case. It must be provided values for Case.In, and Case.Exp or Case.Not or Case.Pass at least.

type CheckResult

type CheckResult struct {
	// Passed is true if the current check passed
	Passed bool
	// Reason is the string output of a failed test as returned by a
	// check.Explainer, typically in format "exp X, got Y".
	Reason string
	// contains filtered or unexported fields
}

CheckResult is a single check result after a dry run.

func (CheckResult) String

func (cr CheckResult) String() string

type HTTPHandlerRunner

type HTTPHandlerRunner interface {
	Runner
	// DryRun returns a HandlerResulter to access test results
	// without running *testing.T.
	DryRun() HandlerResulter
	// WithRequest sets the input request to call the handler with.
	// If not set, the following default request is used:
	//	http.NewRequest("GET", "/", nil)
	WithRequest(*http.Request) HTTPHandlerRunner
	// Request adds checkers on the resulting request,
	// after the last middleware is called and before the handler is called.
	Request(...check.HTTPRequestChecker) HTTPHandlerRunner
	// Response adds checkers on the written response.
	Response(...check.HTTPResponseChecker) HTTPHandlerRunner
	// Duration adds checkers on the handler's execution time;
	Duration(...check.DurationChecker) HTTPHandlerRunner
}

HTTPHandlerRunner provides methods to run tests on http handlers and middlewares.

func HTTPHandler

func HTTPHandler(
	h http.Handler,
	middlewares ...func(http.Handler) http.Handler,
) HTTPHandlerRunner

HTTPHandler returns a HandlerRunner to run tests on http handlers and middlewares.

Example (Middlewares)
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/drykit-go/testx"
	"github.com/drykit-go/testx/check"
)

func main() {
	// withLongProcess middleware processes something for 100 milliseconds.
	withLongProcess := func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			time.Sleep(100 * time.Millisecond)
			next.ServeHTTP(w, r)
		})
	}

	// withContextValue middleware attaches the input key-val pair
	// to the request context.
	withContextValue := func(key, val interface{}) func(http.Handler) http.Handler {
		return func(next http.Handler) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				ctx := context.WithValue(r.Context(), key, val)
				next.ServeHTTP(w, r.WithContext(ctx))
			})
		}
	}

	// withContentType middleware sets the response header Content-Type
	// to contentType.
	withContentType := func(contentType string) func(http.Handler) http.Handler {
		return func(next http.Handler) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.Header().Set("Content-Type", contentType)
				next.ServeHTTP(w, r)
			})
		}
	}

	results := testx.HTTPHandler(nil, // nil is interpreted as a no-op handler
		withLongProcess,
		withContextValue("userID", 42),
		withContentType("application/json"),
	).
		Duration(check.Duration.Over(100 * time.Millisecond)).
		Request(check.HTTPRequest.Context(check.Context.HasKeys("userID"))).
		Response(check.HTTPResponse.Header(check.HTTPHeader.HasValue("application/json"))).
		DryRun()

	fmt.Println(results.Passed())

}
Output:

true

func HTTPHandlerFunc

func HTTPHandlerFunc(
	hf http.HandlerFunc,
	middlewareFuncs ...func(http.HandlerFunc) http.HandlerFunc,
) HTTPHandlerRunner

HTTPHandlerFunc returns a HandlerRunner to run tests on http handlers and middlewares.

Example
package main

import (
	"net/http"
	"testing"
	"time"

	"github.com/drykit-go/testx"
	"github.com/drykit-go/testx/check"
)

func MyHTTPHandler(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")
	if id != "42" {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}
	time.Sleep(500 * time.Millisecond)
	w.Write([]byte("ok"))
}

func main() {
	t := &testing.T{} // ignore: emulating a testing context

	t.Run("good request", func(t *testing.T) {
		r, _ := http.NewRequest("GET", "/endpoint?id=42", nil)
		testx.HTTPHandlerFunc(MyHTTPHandler).WithRequest(r).
			Response(
				check.HTTPResponse.StatusCode(check.Int.InRange(200, 299)),
				check.HTTPResponse.Body(check.Bytes.Is([]byte("ok"))),
			).
			Run(t)
	})

	t.Run("bad request", func(t *testing.T) {
		r, _ := http.NewRequest("GET", "/endpoint?id=404", nil)
		testx.HTTPHandlerFunc(MyHTTPHandler).WithRequest(r).
			Response(check.HTTPResponse.Status(check.String.Contains("Not Found"))).
			Duration(check.Duration.Under(10 * time.Millisecond)).
			Run(t)
	})
}
Output:

Example (DryRun)
package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/drykit-go/testx"
	"github.com/drykit-go/testx/check"
)

func MyHTTPHandler(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")
	if id != "42" {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}
	time.Sleep(500 * time.Millisecond)
	w.Write([]byte("ok"))
}

func main() {
	handlerRunner := testx.HTTPHandlerFunc(MyHTTPHandler)

	goodRequest, _ := http.NewRequest("GET", "/endpoint?id=42", nil)
	goodRequestResults := handlerRunner.WithRequest(goodRequest).
		Response(
			check.HTTPResponse.StatusCode(check.Int.InRange(200, 299)),
			check.HTTPResponse.Body(check.Bytes.Is([]byte("ok"))),
		).
		DryRun()

	badRequest, _ := http.NewRequest("GET", "/endpoint?id=404", nil)
	badRequestResults := handlerRunner.WithRequest(badRequest).
		Response(check.HTTPResponse.Status(check.String.Contains("Not Found"))).
		Duration(check.Duration.Under(10 * time.Millisecond)).
		DryRun()

	fmt.Println(goodRequestResults.Passed())
	fmt.Println(goodRequestResults.ResponseStatus())
	fmt.Println(badRequestResults.Passed())
	fmt.Println(badRequestResults.ResponseStatus())

}
Output:

true
200 OK
true
404 Not Found

type HandlerResulter

type HandlerResulter interface {
	Resulter
	// ResponseHeader returns the gotten response header.
	ResponseHeader() http.Header
	// ResponseStatus returns the gotten response status.
	ResponseStatus() string
	// ResponseCode returns the gotten response code.
	ResponseCode() int
	// ResponseBody returns the gotten response body.
	ResponseBody() []byte
	// ResponseDuration returns the handler's execution time.
	ResponseDuration() time.Duration
}

HandlerResulter provides methods to read HandlerRunner results after a dry run.

type Resulter

type Resulter interface {
	// Checks returns a slice of CheckResults listing the runned checks
	Checks() []CheckResult
	// Passed returns true if all checks passed.
	Passed() bool
	// Failed returns true if one check or more failed.
	Failed() bool
	// NChecks returns the number of checks.
	NChecks() int
	// NPassed returns the number of checks that passed.
	NPassed() int
	// NFailed returns the number of checks that failed.
	NFailed() int
}

Resulter provides methods to read test results after a dry run.

type Runner

type Runner interface {
	// Run runs a test and fails it if a check does not pass.
	Run(t *testing.T)
}

Runner provides a method Run that runs a test.

type TableConfig

type TableConfig struct {
	// InPos is the nth parameter in which Case.In value is injected,
	// starting at 0.
	// It is required if the tested func accepts multiple parameters.
	// Default is 0.
	InPos int

	// OutPos is the nth return value that is tested against Case.Exp,
	// starting at 0.
	// It is required if the tested func returns multiple values.
	// Default is 0.
	OutPos int

	// FixedArgs is a slice of arguments to be injected into the tested func.
	// Its values are fixed for all cases.
	// It is required if the tested func accepts multiple parameters.
	//
	// Let nparams the number of parameters of the tested func, len(FixedArgs)
	// must equal nparams or nparams - 1.
	//
	// The following configurations produce the same result:
	//
	// 	testx.Table(myFunc).Config(testx.TableConfig{
	// 		InPos: 1
	// 		FixedArgs: []interface{"myArg0", "myArg2"} // len(FixedArgs) == 2
	// 	})
	//
	// 	testx.Table(myFunc).Config(testx.TableConfig{
	// 		InPos: 1
	// 		FixedArgs: []interface{0: "myArg0", 2: "myArg2"} // len(FixedArgs) == 3
	// 	})
	FixedArgs Args
}

TableConfig is configuration object for TableRunner. It allows to test functions having multiple parameters or multiple return values. Its zero value is a valid config for functions of 1 parameter and 1 return value, so it can be omitted in that case.

type TableResulter

type TableResulter interface {
	Resulter
	// PassedAt returns true if the ith test case passed.
	PassedAt(index int) bool
	// FailedAt returns true if the ith test case failed.
	FailedAt(index int) bool
	// PassedLabel returns true if the test case with matching label passed.
	PassedLabel(label string) bool
	// FailedLabel returns true if the test case with matching label failed.
	FailedLabel(label string) bool
}

TableResulter provides methods to read TableRunner results after a dry run.

type TableRunner

type TableRunner interface {
	Runner
	// DryRun returns a TableResulter to access test results
	// without running *testing.T.
	DryRun() TableResulter
	// Config sets configures the TableRunner for functions of multiple
	// parameters or multiple return values.
	Config(cfg TableConfig) TableRunner
	// Cases adds test cases to be run on the tested func.
	Cases(cases []Case) TableRunner
}

TableRunner provides methods to run a series of test cases on a single function.

func Table

func Table(testedFunc interface{}) TableRunner

Table returns a TableRunner to run test cases on a func. By default, it works with funcs having a single input and output value. Use TableRunner.Config to configure it for a more complex functions.

Example (Dyadic)
package main

import (
	"errors"
	"testing"

	"github.com/drykit-go/testx"
)

func main() {
	t := &testing.T{} // ignore: emulating a testing context

	// divide is the func to be tested.
	// It returns x/y or a non-nil error if y == 0
	divide := func(x, y float64) (float64, error) {
		if y == 0 {
			return 0, errors.New("division by 0")
		}
		return x / y, nil
	}

	// func divide has several parameters and return values,
	// so we specify a config to determinate:
	// - at which param position Case.In is injected
	// - the values used for the other arguments (fixed for all cases)
	// - which return value we want to compare Case.Exp with
	//
	// In this example, we check the error value of divide (return value
	// at position 1).
	// We inject Case.In at position 1 (param y) and use a fixed value
	// of 42.0 at position 0 (param x) for all cases.
	testx.Table(divide).Config(testx.TableConfig{
		// Positions start at 0
		InPos:     1,                      // Case.In injected in param position 1 (y)
		OutPos:    1,                      // Case.Exp compared to return value position 1 (error value)
		FixedArgs: []interface{}{0: 42.0}, // param 0 (x) set to 42.0 for all cases
	}).Cases([]testx.Case{
		{In: 1.0, Exp: testx.ExpNil},                // divide(42.0, 1.0) -> (_, nil)
		{In: 0.0, Exp: errors.New("division by 0")}, // divide(42.0, 0.0) -> (_, err)
	}).Run(t)
}
Output:

Example (Monadic)
package main

import (
	"testing"

	"github.com/drykit-go/testx"
	"github.com/drykit-go/testx/check"
	"github.com/drykit-go/testx/checkconv"
)

func main() {
	t := &testing.T{} // ignore: emulating a testing context

	// double is the func to be tested.
	double := func(x float64) float64 { return 2 * x }

	// func double has 1 parameter and 1 return value,
	// hence no config is needed
	testx.Table(double).Cases([]testx.Case{
		{In: 0.0, Exp: 0.0},
		{In: -2.0, Pass: checkconv.AssertMany(check.Float64.InRange(-5, -3))},
	}).Run(t)
}
Output:

type ValueRunner

type ValueRunner interface {
	Runner
	// DryRun returns a Resulter to access test results
	// without running *testing.T.
	DryRun() Resulter
	// Exp adds an equality check on the tested value.
	Exp(value interface{}) ValueRunner
	// Not adds inequality checks on the tested value.
	Not(values ...interface{}) ValueRunner
	// Pass adds checkers on the tested value.
	Pass(checkers ...check.ValueChecker) ValueRunner
}

ValueRunner provides methods to perform tests on a single value.

Example
package main

import (
	"testing"

	"github.com/drykit-go/testx"
	"github.com/drykit-go/testx/check"
	"github.com/drykit-go/testx/checkconv"
)

func main() {
	t := &testing.T{}                 // ignore: emulating a testing context
	get42 := func() int { return 42 } // Some dummy func

	// Run Value test via Run(t *testing.T)
	testx.Value(get42()).
		Exp(42).                                           // pass
		Not(3, "hello").                                   // pass
		Pass(checkconv.Assert(check.Int.InRange(41, 43))). // pass
		Run(t)
}
Output:

Example (DryRun)
package main

import (
	"fmt"

	"github.com/drykit-go/testx"
	"github.com/drykit-go/testx/check"
	"github.com/drykit-go/testx/checkconv"
)

func main() {
	get42 := func() int { return 42 } // Some dummy func

	results := testx.Value(get42()).
		Exp(42). // pass
		Pass(checkconv.AssertMany(
			check.Int.GTE(41),        // pass
			check.Int.InRange(-1, 1), // fail
		)...).
		DryRun()

	fmt.Println(results.Passed())
	fmt.Println(results.Failed())
	fmt.Println(results.Checks())
	fmt.Println(results.NPassed())
	fmt.Println(results.NFailed())
	fmt.Println(results.NChecks())

}
Output:

false
true
[{passed} {passed} {failed value:
exp in range [-1:1]
got 42}]
2
1
3

func Value

func Value(v interface{}) ValueRunner

Value returns a ValueRunner to run tests on a single value.

Directories

Path Synopsis
Package check provides types to perform checks on values in a testing context.
Package check provides types to perform checks on values in a testing context.
Package checkconv provides functions to convert typed checkers into generic ones.
Package checkconv provides functions to convert typed checkers into generic ones.
cmd
gen
internal
fmtexpl
Package fmtexpl provides functions to format failed test explanations.
Package fmtexpl provides functions to format failed test explanations.
gen

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL