tf

package module
v1.7.0 Latest Latest
Warning

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

Go to latest
Published: May 30, 2019 License: MIT Imports: 14 Imported by: 0

README

tf

tf is a microframework for parametrized testing of functions and HTTP in Go.

Functions

It offers a simple and intuitive syntax for tests by wrapping the function:

// Remainder returns the quotient and remainder from dividing two integers.
func Remainder(a, b int) (int, int) {
    return a / b, a % b
}

func TestRemainder(t *testing.T) {
    Remainder := tf.Function(t, Remainder)

    Remainder(10, 3).Returns(3, 1)
    Remainder(10, 2).Returns(5, 0)
    Remainder(17, 7).Returns(2, 3)
}

Assertions are performed with testify. If an assertion fails it will point to the correct line so you do not need to explicitly label tests.

The above test will output (in verbose mode):

=== RUN   TestRemainder
--- PASS: TestRemainder (0.00s)
=== RUN   TestRemainder/Remainder#1
--- PASS: TestRemainder/Remainder#1 (0.00s)
=== RUN   TestRemainder/Remainder#2
--- PASS: TestRemainder/Remainder#2 (0.00s)
=== RUN   TestRemainder/Remainder#3
--- PASS: TestRemainder/Remainder#3 (0.00s)
PASS

Grouping

Use NamedFunction to specify a custom name for the function/group:

func TestNamedSum(t *testing.T) {
	Sum := tf.NamedFunction(t, "Sum1", Item.Add)

	Sum(Item{1.3, 4.5}, 3.4).Returns(9.2)
	Sum(Item{1.3, 4.6}, 3.5).Returns(9.4)

	Sum = tf.NamedFunction(t, "Sum2", Item.Add)

	Sum(Item{1.3, 14.5}, 3.4).Returns(19.2)
	Sum(Item{21.3, 4.6}, 3.5).Returns(29.4)
}

Struct Functions

You can test struct functions by providing the struct value as the first parameter followed by any function arguments, if any.

type Item struct {
	a, b float64
}

func (i Item) Add(c float64) float64 {
	return i.a + i.b + c
}

func TestItem_Add(t *testing.T) {
	Sum := tf.Function(t, Item.Add)

	Sum(Item{1.3, 4.5}, 3.4).Returns(9.2)
}

HTTP Testing

Client

Super easy HTTP testing by using the ServeHTTP function. This means that you do not have to run the server and it is compatible with all HTTP libraries and frameworks but has all the functionality of the server itself.

The simplest example is to use the default muxer in the http package:

http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, World!")
})

And now we can write some tests:

func TestHTTPRouter(t *testing.T) {
	run := tf.ServeHTTP(t, http.DefaultServeMux.ServeHTTP)

	run(&tf.HTTPTest{
		Path:         "/hello",
		Status:       http.StatusOK,
		ResponseBody: strings.NewReader("Hello, World!"),
	})

	run(&tf.HTTPTest{
		Path:   "/world",
		Status: http.StatusNotFound,
	})
}

It is compatible with all HTTP frameworks because they must all expose a ServeHTTP which is the entry point for the request router/handler.

There are many more options for HTTPTest. Some HTTP tests require multiple operations, you can use MultiHTTPTest for this:

run(&tf.MultiHTTPTest{
	Steps: []*tf.HTTPTest{
		{
			Path:        "/save",
			Method:      http.MethodPut,
			RequestBody: strings.NewReader(`{"foo":"bar"}`),
			Status:      http.StatusCreated,
		},
		{
			Path:         "/fetch",
			Method:       http.MethodGet,
			Status:       http.StatusOK,
			ResponseBody: strings.NewReader(`{"foo":"bar"}`),
		},
	},
})

Each step will only proceed if the previous step was successful.

Server

Sometimes you need to mock HTTP servers where the only option is to provide a URL endpoint through to your test. That is, when you do not have direct access to the router, or it's impractical to inject the behavior.

For case this you can use a HTTPServer:

// 0 means to use a random port, or you can provide your own.
server := tf.StartHTTPServer(0).
	AddHandlers(map[string]http.HandlerFunc{
		"/hi": func(w http.ResponseWriter, r *http.Request) {
			w.Write([]byte(`hello`))
		},
		"/easy": tf.HTTPJSONResponse(200, []int{1, 2, 3}),
	})

// Always remember to tear down the resources when the test ends.
defer server.Shutdown()

// Your test code here...
server.Endpoint() // http://localhost:61223

Using a real HTTP server has some benefits:

  1. It's isolated. That means it does not interfere in anyway with the global HTTP server.

  2. It can be used in parallel. You can either share the same HTTPServer across many tests (such as in TestMain), or create one for each test in parallel. Providing 0 for the port (as in the example above) will ensure that it always selects an unused random port.

  3. It mutable. After creating and starting the HTTPServer you can add/remove handlers. This is useful when most tests need a base logic, but some cases need to return special under specific scenarios.

You can create your own handlers, of course, but there are a few common ones that also ship with tf:

  • HTTPEmptyResponse(statusCode int)
  • HTTPStringResponse(statusCode int, body string)
  • HTTPJSONResponse(statusCode int, body interface{})

Environment Variables

SetEnv sets an environment variable and returns a reset function to ensure the environment is always returned to it's previous state:

resetEnv := tf.SetEnv(t, "HOME", "/somewhere/else")
defer resetEnv()

If you would like to set multiple environment variables, you can use SetEnvs in the same way:

resetEnv := tf.SetEnvs(t, map[string]string{
    "HOME":  "/somewhere/else",
    "DEBUG": "on",
})
defer resetEnv()

Documentation

Overview

Package tf is a microframework for parametrized testing of functions.

I wrote this because I was tired of creating a []struct{} fixture for most of my tests. I knew there had to be an easier and more reliable way.

It offers a simple and intuitive syntax for tests by wrapping the function:

// Remainder returns the quotient and remainder from dividing two integers.
func Remainder(a, b int) (int, int) {
    return a / b, a % b
}

func TestRemainder(t *testing.T) {
    Remainder := tf.Function(t, Remainder)

    Remainder(10, 3).Returns(3, 1)
    Remainder(10, 2).Returns(5, 0)
    Remainder(17, 7).Returns(2, 3)
}

Assertions are performed with github.com/stretchr/testify/assert. If an assertion fails it will point to the correct line so you do not need to explicitly label tests.

The above test will output (in verbose mode):

=== RUN   TestRemainder
--- PASS: TestRemainder (0.00s)
=== RUN   TestRemainder/Remainder#1
--- PASS: TestRemainder/Remainder#1 (0.00s)
=== RUN   TestRemainder/Remainder#2
--- PASS: TestRemainder/Remainder#2 (0.00s)
=== RUN   TestRemainder/Remainder#3
--- PASS: TestRemainder/Remainder#3 (0.00s)
PASS

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Function

func Function(t *testing.T, fn interface{}) func(args ...interface{}) *F

Function wraps fn into F testing type and returns back function to which you can use as a regular function in e.g:

// Remainder returns the quotient and remainder from dividing two integers.
func Remainder(a, b int) (int, int) {
    return a / b, a % b
}

func TestRemainder(t *testing.T) {
    Remainder := tf.Function(t, Remainder)

    Remainder(10, 3).Returns(3, 1)
    Remainder(10, 2).Returns(5, 0)
    Remainder(17, 7).Returns(2, 3)
}

func HTTPEmptyResponse added in v1.7.0

func HTTPEmptyResponse(statusCode int) func(http.ResponseWriter, *http.Request)

func HTTPJSONResponse added in v1.7.0

func HTTPJSONResponse(statusCode int, body interface{}) func(http.ResponseWriter, *http.Request)

func HTTPStringResponse added in v1.7.0

func HTTPStringResponse(statusCode int, body string) func(http.ResponseWriter, *http.Request)

func NamedFunction added in v1.5.0

func NamedFunction(t *testing.T, fnName string, fn interface{}) func(args ...interface{}) *F

NamedFunction works the same way as Function but allows a custom name for the function to be set.

This is especially important when the function is anonymous, or it can be used for grouping tests:

Sum := tf.NamedFunction(t, "Sum1", Item.Add)

Sum(Item{1.3, 4.5}, 3.4).Returns(9.2)
Sum(Item{1.3, 4.6}, 3.5).Returns(9.4)

Sum = tf.NamedFunction(t, "Sum2", Item.Add)

Sum(Item{1.3, 14.5}, 3.4).Returns(19.2)
Sum(Item{21.3, 4.6}, 3.5).Returns(29.4)

func ServeHTTP added in v1.1.0

func ServeHTTP(t *testing.T, handlerFunc http.HandlerFunc) func(HTTPTester)

func SetEnv added in v1.6.0

func SetEnv(t *testing.T, name, value string) (resetEnv func())

SetEnv sets an environment variable and returns a reset function to ensure the environment is always returned to it's previous state:

resetEnv := tf.SetEnv(t, "HOME", "/somewhere/else")
defer resetEnv()

If you would like to set multiple environment variables, see SetEnvs().

func SetEnvs added in v1.6.0

func SetEnvs(t *testing.T, env map[string]string) (resetEnv func())

SetEnvs works the same way as SetEnv, but on multiple environment variables:

resetEnv := tf.SetEnvs(t, map[string]string{
    "HOME":  "/somewhere/else",
    "DEBUG": "on",
})
defer resetEnv()

Types

type F

type F struct {
	// contains filtered or unexported fields
}

F wrapper around a func which handles testing instance, agrs and reveals function name

func (*F) Errors added in v1.4.0

func (f *F) Errors(args ...interface{})

Errors check if function returns errors and match expectation you provided

BasicErrorer := tf.Function(t, func() error { return errors.New("some error") } )
BasicErrorer().Errors()

You also can provide strings to match message

AdvancedErrorer := tf.Function(t, func() error { return errors.New("some error") } )
AdvancedErrorer().Errors("some error")

Or you may provide your custom error type to check it bumps correctly

 custom := MyCustomError{errors.New("some error")}
	TypeErrorer := tf.Function(t, func() error { return custom } )
	TypeErrorer().Errors(custom)

func (*F) False added in v1.2.0

func (f *F) False()

False matches is function returns false as a result

func Switch() bool {
    return false
}

func TestSwitch(t *testing.T) {
    Switch := tf.Function(t, Switch)

    Switch().False()
}

func (*F) Returns

func (f *F) Returns(expected ...interface{})

Returns matches if expected result matches actual

Remainder := tf.Function(t, func(a,b int) int { return a + b })
Remainder(1, 2).Returns(3)

func (*F) True added in v1.2.0

func (f *F) True()

True matches is function returns true as a result

func Switch() bool {
    return true
}

func TestSwitch(t *testing.T) {
    Switch := tf.Function(t, Switch)

    Switch().True()
}

type HTTPBeforeFunc added in v1.1.0

type HTTPBeforeFunc func(request *http.Request, response *httptest.ResponseRecorder)

type HTTPCheckFunc added in v1.1.0

type HTTPCheckFunc func(t *testing.T, request *http.Request, response *httptest.ResponseRecorder) bool

type HTTPFinallyFunc added in v1.1.0

type HTTPFinallyFunc func(request *http.Request, response *httptest.ResponseRecorder)

type HTTPServer added in v1.7.0

type HTTPServer struct {
	Server   *http.Server
	Port     int
	Shutdown func()
	Mux      *http.ServeMux
}

func StartHTTPServer added in v1.7.0

func StartHTTPServer(port int) *HTTPServer

func (*HTTPServer) AddHandler added in v1.7.0

func (server *HTTPServer) AddHandler(path string, handler http.HandlerFunc) *HTTPServer

func (*HTTPServer) AddHandlers added in v1.7.0

func (server *HTTPServer) AddHandlers(handlers map[string]http.HandlerFunc) *HTTPServer

func (*HTTPServer) Endpoint added in v1.7.0

func (server *HTTPServer) Endpoint() string

type HTTPTest added in v1.1.0

type HTTPTest struct {
	// Name is used as the test name. If it is empty the test name will be based
	// on the Path.
	Name string

	// Method is the HTTP request method. If blank then "GET" will be used.
	Method string

	// Path used in the request. If the Path is blank then "/" is used because
	// it is not possible to parse an empty path.
	Path string

	// RequestBody is the body for the request. You use a string as the body
	// with:
	//
	//   RequestBody: strings.NewReader("foo bar")
	//
	RequestBody io.Reader

	// RequestHeaders will add or replace any header on the request.
	RequestHeaders map[string]string

	// ResponseHeaders will be checked from the response. Only the headers in
	// ResponseHeaders will be checked and and their values must be exactly
	// equal.
	//
	// If you need to do more sophisticated checking or headers you should use
	// Check.
	ResponseHeaders map[string]string

	// ResponseBody will check the body of the response. ResponseBody must be
	// not nil for the check to occur.
	//
	// You can check a string with:
	//
	//   ResponseBody: strings.NewReader("foo bar")
	//
	ResponseBody io.Reader

	// Status is the expected response HTTP status code. You can use one of the
	// constants in the http package such as http.StatusOK. If Status is not
	// provided then the response status will not be checked.
	Status int

	// Check is an optional function that is run before any other assertions. It
	// receives the request and response so you can do any custom validation. If
	// Check returns true the built in assertions will continue. Otherwise a
	// return value of false means to stop checking the response because an
	// error has already been found.
	Check HTTPCheckFunc

	// Finally is always called as a last event, even if the test fails. It is
	// useful for guaranteeing cleanup or restoration of environments.
	//
	// The return value is ignored.
	Finally HTTPFinallyFunc

	// Before is run after the request and record is setup but before the
	// request is executed.
	Before HTTPBeforeFunc
}

func (*HTTPTest) RealPath added in v1.1.0

func (ht *HTTPTest) RealPath() string

func (*HTTPTest) TestName added in v1.1.0

func (ht *HTTPTest) TestName() string

func (*HTTPTest) Tests added in v1.1.0

func (ht *HTTPTest) Tests() []*HTTPTest

type HTTPTester added in v1.1.0

type HTTPTester interface {
	TestName() string
	Tests() []*HTTPTest
}

type MultiHTTPTest added in v1.1.0

type MultiHTTPTest struct {
	// Name is used as the test name. If it is empty the test name will be based
	// on the Path.
	Name string

	// Before is run before any of the Steps begin.
	Before func()

	Steps []*HTTPTest
}

func (*MultiHTTPTest) TestName added in v1.1.0

func (ht *MultiHTTPTest) TestName() string

func (*MultiHTTPTest) Tests added in v1.1.0

func (ht *MultiHTTPTest) Tests() []*HTTPTest

Jump to

Keyboard shortcuts

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