forest

package module
v1.7.1 Latest Latest
Warning

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

Go to latest
Published: Nov 10, 2023 License: MIT Imports: 16 Imported by: 0

README

forest - for testing REST api-s in Go

Build Status Go Report Card GoDoc

This package provides a few simple helper types and functions to create functional tests that call a running REST based WebService.

install

go get github.com/emicklei/forest

simple example

package main

import (
    "net/http"
    "testing"

    . "github.com/emicklei/forest"
)

var github = NewClient("https://api.github.com", new(http.Client))

func TestForestProjectExists(t *testing.T) {
    cfg := NewConfig("/repos/emicklei/{repo}", "forest").Header("Accept", "application/json")
    r := github.GET(t, cfg)
    ExpectStatus(t, r, 200)
}

graphql support

query := forest.NewGraphQLRequest(list_matrices_query, "ListMatrices")
query, err = query.WithVariablesFromString(`

{ "repositoryID":"99426e24-..........-6bf9770f1fd5", "page":{ "first":20 }, }`) // ... handle error cfg := forest.NewRequestConfig(...) cfg.Content(query, "application/json") r := SkillsAPI.POST(t, cfg) ExpectStatus(t, r, 200)

other helper functions

func ExpectHeader(t T, r *http.Response, name, value string)
func ExpectJSONArray(t T, r *http.Response, callback func(array []interface{}))
func ExpectJSONDocument(t T, r *http.Response, doc interface{})
func ExpectJSONHash(t T, r *http.Response, callback func(hash map[string]interface{}))
func ExpectStatus(t T, r *http.Response, status int) bool
func ExpectString(t T, r *http.Response, callback func(content string))
func ExpectXMLDocument(t T, r *http.Response, doc interface{})
func JSONArrayPath(t T, r *http.Response, dottedPath string) interface{}
func JSONPath(t T, r *http.Response, dottedPath string) interface{}
func ProcessTemplate(t T, templateContent string, value interface{}) string
func Scolorf(syntaxCode string, format string, args ...interface{}) string
func SkipUnless(s skippeable, labels ...string)
func XMLPath(t T, r *http.Response, xpath string) interface{}
func Dump(t T, resp *http.Response)

more docs

Introduction Blog Post

© 2016+, http://ernestmicklei.com. MIT License. Contributions welcome.

Documentation

Overview

Package forest has functions for REST Api testing in Go

This package provides a few simple helper types and functions to create functional tests that call HTTP services. A test uses a forest Client which encapsulates a standard http.Client and a base url. Such a client can be created inside a function or by initializing a package variable for shared access. Using a client, you can send http requests and call multiple expectation functions on each response.

Most functions of the forest package take the *testing.T variable as an argument to report any error.

Example

// setup a shared client to your API
var chatter = forest.NewClient("http://api.chatter.com", new(http.Client))

func TestGetMessages(t *testing.T) {
	r := chatter.GET(t, forest.Path("/v1/messages").Query("user","zeus"))
	ExpectStatus(t,r,200)
	ExpectJSONArray(t,r,func(messages []interface{}){

		// in the callback you can validate the response structure
		if len(messages) == 0 {
			t.Error("expected messages, got none")
		}
	})
}

To compose http requests, you create a RequestConfig value which as a Builder interface for setting the path,query,header and body parameters. The ProcessTemplate function can be useful to create textual payloads. To inspect http responses, you use the Expect functions that perform the unmarshalling or use XMLPath or JSONPath functions directly on the response.

If needed, implement the standard TestMain to do global setup and teardown.

func TestMain(m *testing.M) {
	// there is no *testing.T available, use an stdout implementation
	t := forest.TestingT

	// setup
	chatter.PUT(t, forest.Path("/v1/messages/{id}",1).Body("<payload>"))
	ExpectStatus(t,r,204)

	exitCode := m.Run()

	// teardown
	chatter.DELETE(t, forest.Path("/v1/messages/{id}",1))
	ExpectStatus(t,r,204)

	os.Exit(exitCode)
}

Special features

In contrast to the standard behavior, the Body of a http.Response is made re-readable. This means one can apply expectations to a response as well as dump the full contents.

The function XMLPath provides XPath expression support. It uses the [https://godoc.org/launchpad.net/xmlpath] package. The similar function JSONPath can be used on JSON documents.

Colorizes error output (can be configured using package vars).

All functions can also be used in a setup and teardown as part of TestMain.

(c) 2015, http://ernestmicklei.com. MIT License

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrorColorSyntaxCode = "@{wR}"

ErrorColorSyntaxCode requires the syntax defined on https://github.com/wsxiaoys/terminal/blob/master/color/color.go . Set to an empty string to disable coloring.

View Source
var FatalColorSyntaxCode = "@{wR}"

FatalColorSyntaxCode requires the syntax defined on https://github.com/wsxiaoys/terminal/blob/master/color/color.go . Set to an empty string to disable coloring.

View Source
var LoggingPrintf = fmt.Printf

LoggingPrintf is the function used by TestingT to produce logging on Logf,Error and Fatal.

View Source
var MaskChar = "*"

MaskChar is used the create a masked header value.

View Source
var NewRequestConfig = NewConfig

NewRequestConfig is an alias for NewConfig

View Source
var Path = NewConfig

Path is an alias for NewConfig

View Source
var TerminalColorsEnabled = true

TerminalColorsEnabled can be changed to disable the use of terminal coloring. One usecase is to add a command line flag to your test that controls its value.

func init() {
	flag.BoolVar(&forest.TerminalColorsEnabled, "color", true, "want colors?")
}

go test -color=false
View Source
var TestingT = Logger{InfoEnabled: true, ErrorEnabled: true, ExitOnFatal: true}

TestingT provides a sub-api of testing.T. Its purpose is to allow the use of this package in TestMain(m).

Functions

func CheckError

func CheckError(t T, err error) bool

CheckError simply tests the error and fail is not undefined. This is implicity called after sending a Http request. Return true if there was an error.

func CookieNamed added in v1.3.0

func CookieNamed(resp *http.Response, name string) *http.Cookie

CookieNamed returns the cookie with matching name. Returns nil if not found.

func Dump

func Dump(t T, resp *http.Response)

Dump is a convenient method to log the full contents of a request and its response.

func Errorf

func Errorf(t *testing.T, format string, args ...interface{})

Errorf calls Error on t with a colorized message

func ExpectHeader

func ExpectHeader(t T, r *http.Response, name, value string) bool

ExpectHeader inspects the header of the response. Return true if the header matches.

Example
var t *testing.T

yourAPI := NewClient("http://api.yourservices.com", new(http.Client)) // yourAPI could be a package variable

r := yourAPI.GET(t, Path("/v1/assets/artreyu").Header("Accept", "application/xml"))
ExpectHeader(t, r, "Content-Type", "application/xml")
Output:

func ExpectJSONArray

func ExpectJSONArray(t T, r *http.Response, callback func(array []interface{})) bool

ExpectJSONArray tries to unmarshal the response body into a Go slice callback parameter. Fail if the body could not be read or if unmarshalling was not possible. Returns true if the callback was executed with an array.

Example
var t *testing.T

yourAPI := NewClient("http://api.yourservices.com", new(http.Client)) // yourAPI could be a package variable

r := yourAPI.GET(t, Path("/v1/assets").Header("Content-Type", "application/json"))
ExpectJSONArray(t, r, func(array []interface{}) {
	// here you should inspect the array for expected content
	// and use t (*testing.T) to report a failure.
})
Output:

func ExpectJSONDocument

func ExpectJSONDocument(t T, r *http.Response, doc interface{}) bool

ExpectJSONDocument tries to unmarshal the response body into fields of the provided document (struct). Fail if the body could not be read or unmarshalled. Returns true if a document could be unmarshalled.

func ExpectJSONHash

func ExpectJSONHash(t T, r *http.Response, callback func(hash map[string]interface{})) bool

ExpectJSONHash tries to unmarshal the response body into a Go map callback parameter. Fail if the body could not be read or if unmarshalling was not possible. Returns true if the callback was executed with a map.

Example
var t *testing.T

yourAPI := NewClient("http://api.yourservices.com", new(http.Client)) // yourAPI could be a package variable

r := yourAPI.GET(t, Path("/v1/assets/artreyu/descriptor").Header("Content-Type", "application/json"))
ExpectJSONHash(t, r, func(hash map[string]interface{}) {
	// here you should inspect the hash for expected content
	// and use t (*testing.T) to report a failure.
})
Output:

func ExpectStatus

func ExpectStatus(t T, r *http.Response, status int) bool

ExpectStatus inspects the response status code. If the value is not expected, the complete request, response is logged (iff verboseOnFailure) and the test is aborted. Return true if the status is as expected.

Example
var t *testing.T

yourAPI := NewClient("http://api.yourservices.com", new(http.Client)) // yourAPI could be a package variable

r := yourAPI.GET(t, Path("/v1/assets/artreyu").Header("Accept", "application/xml"))
ExpectStatus(t, r, 200)
Output:

func ExpectString

func ExpectString(t T, r *http.Response, callback func(content string)) bool

ExpectString reads the response body into a Go string callback parameter. Fail if the body could not be read or unmarshalled. Returns true if a response body was read.

func ExpectXMLDocument

func ExpectXMLDocument(t T, r *http.Response, doc interface{}) bool

ExpectXMLDocument tries to unmarshal the response body into fields of the provided document (struct). Fail if the body could not be read or unmarshalled. Returns true if a document could be unmarshalled.

Example

How to use the ExpectXMLDocument function on a http response.

var t *testing.T

yourAPI := NewClient("http://api.yourservices.com", new(http.Client)) // yourAPI could be a package variable

r := yourAPI.GET(t, Path("/v1/assets").Header("Accept", "application/xml"))

var root YourType // YourType must reflect the expected document structure
ExpectXMLDocument(t, r, &root)
// here you should inspect the root (instance of YourType) for expected field values
// and use t (*testing.T) to report a failure.
Output:

func Fatalf

func Fatalf(t *testing.T, format string, args ...interface{})

Fatalf calls Fatal on t with a colorized message

func IsMaskedHeader added in v1.1.0

func IsMaskedHeader(name string) bool

IsMaskedHeader return true if the name is part of (case-insensitive match) the MaskHeaderNames.

func JSONArrayPath

func JSONArrayPath(t T, r *http.Response, dottedPath string) interface{}

JSONArrayPath returns the value found by following the dotted path in a JSON array. E.g .1.title in [ {"title":"Go a long way"}, {"title":"scary scala"} ]

Example
var t *testing.T

yourAPI := NewClient("http://api.yourservices.com", new(http.Client)) // yourAPI could be a package variable

r := yourAPI.GET(t, Path("/v1/assets").Header("Content-Type", "application/json"))
// if the content looks like this
// [
// { "id" : "artreyu", "type" : "tool" }
// ]
// then you can verify it using
if got, want := JSONArrayPath(t, r, ".0.type"), "tool"; got != want {
	t.Errorf("got %v want %v", got, want)
}
Output:

func JSONPath

func JSONPath(t T, r *http.Response, dottedPath string) interface{}

JSONPath returns the value found by following the dotted path in a JSON document hash. E.g .chapters.0.title in { "chapters" : [{"title":"Go a long way"}] }

Example
var t *testing.T

yourAPI := NewClient("http://api.yourservices.com", new(http.Client)) // yourAPI could be a package variable

r := yourAPI.GET(t, Path("/v1/assets/artreyu").Header("Content-Type", "application/json"))
// if the content looks like this
// { "id" : "artreyu", "type" : "tool" }
// then you can verify it using
if got, want := JSONPath(t, r, ".0.id"), "artreyu"; got != want {
	t.Errorf("got %v want %v", got, want)
}
Output:

func Logf

func Logf(t T, format string, args ...interface{})

Logf adds the actual file:line information to the log message

func MaskHeader added in v1.1.0

func MaskHeader(name string)

MaskHeader is used to prevent logging secrets.

func ProcessTemplate

func ProcessTemplate(t T, templateContent string, value interface{}) string

ProcessTemplate creates a new text Template and executes it using the provided value. Returns the string result of applying this template. Failures in the template are reported using t.

func Scolorf

func Scolorf(syntaxCode string, format string, args ...interface{}) string

Scolorf returns a string colorized for terminal output using the syntaxCode (unless that's empty). Requires the syntax defined on https://github.com/wsxiaoys/terminal/blob/master/color/color.go .

func SkipUnless

func SkipUnless(t skippeable, labels ...string)

SkipUnless will Skip the test unless the LABELS environment variable includes any of the provided labels.

LABELS=integration,nightly go test -v
Example
var t *testing.T

// t implements skippeable (has method Skipf)
SkipUnless(t, "nightly")
// code below is executed only if the environment variable LABELS contains `nightly`

// You run the `nightly` tests like this:
//
// LABELS=nightly go test -v
Output:

func URLPathEncode

func URLPathEncode(path string) string

func VerboseOnFailure

func VerboseOnFailure(verbose bool)

VerboseOnFailure (default is false) will produce more information about the request and response when a failure is detected on an expectation. This setting is not the same but related to the value of testing.Verbose().

func XMLPath

func XMLPath(t T, r *http.Response, xpath string) interface{}

XMLPath returns the value found by following the xpath expression in a XML document (payload of response).

Example
var t *testing.T

yourAPI := NewClient("http://api.yourservices.com", new(http.Client)) // yourAPI could be a package variable

r := yourAPI.GET(t, Path("/v1/assets/artreyu").Header("Accept", "application/xml"))
// if the content looks like this
// <?xml version="1.0" ?>
// <asset>
//   <id>artreyu</id>
//   <type>tool</type>
// </asset>
// then you can verify it using
if got, want := XMLPath(t, r, "/asset/id"), "artreyu"; got != want {
	t.Errorf("got %v want %v", got, want)
}
Output:

Types

type APITesting

type APITesting struct {
	BaseURL string
	// contains filtered or unexported fields
}

APITesting provides functions to call a REST api and validate its responses.

func NewClient

func NewClient(baseURL string, httpClient *http.Client) *APITesting

NewClient returns a new ApiTesting that can be used to send Http requests.

func (*APITesting) DELETE

func (a *APITesting) DELETE(t T, config *RequestConfig) *http.Response

DELETE sends a Http request using a config (headers,...) The request is logged and any sending error will fail the test.

func (*APITesting) Do

func (a *APITesting) Do(method string, config *RequestConfig) (*http.Response, error)

Do sends a Http request using a Http method (GET,PUT,POST,....) and config (headers,...) The request is not logged and any URL build error or send error will be returned.

func (*APITesting) GET

func (a *APITesting) GET(t T, config *RequestConfig) *http.Response

GET sends a Http request using a config (headers,...) The request is logged and any sending error will fail the test.

func (*APITesting) PATCH

func (a *APITesting) PATCH(t T, config *RequestConfig) *http.Response

PATCH sends a Http request using a config (headers,...) The request is logged and any sending error will fail the test.

func (*APITesting) POST

func (a *APITesting) POST(t T, config *RequestConfig) *http.Response

POST sends a Http request using a config (headers,body,...) The request is logged and any sending error will fail the test.

func (*APITesting) PUT

func (a *APITesting) PUT(t T, config *RequestConfig) *http.Response

PUT sends a Http request using a config (headers,body,...) The request is logged and any sending error will fail the test.

type GraphQLRequest added in v1.2.0

type GraphQLRequest struct {
	Query         string                 `json:"query,omitempty"`
	OperationName string                 `json:"operationName"`
	Variables     map[string]interface{} `json:"variables"`
}

GraphQLRequest is used to model both a query or a mutation request

func NewGraphQLRequest added in v1.2.0

func NewGraphQLRequest(query, operation string, vars ...Map) GraphQLRequest

NewGraphQLRequest returns a new Request (for query or mutation) without any variables.

func (GraphQLRequest) Reader added in v1.2.0

func (r GraphQLRequest) Reader() io.Reader

Reader returns a new reader for sending it using a HTTP request.

func (GraphQLRequest) WithVariablesFromString added in v1.2.0

func (r GraphQLRequest) WithVariablesFromString(jsonhash string) (GraphQLRequest, error)

WithVariablesFromString returns a copy of the request with decoded variables. Returns an error if the jsonhash cannot be converted.

type JUnitProperty added in v1.7.0

type JUnitProperty struct {
	Name  string `xml:"name,attr"`
	Value string `xml:"value,attr"`
}

type JUnitReport added in v1.7.0

type JUnitReport struct {
	TestSuites []JUnitTestSuite `xml:"testsuite"`
}

func ReadJUnitReport added in v1.7.0

func ReadJUnitReport(filename string) (r JUnitReport, err error)

type JUnitTestCase added in v1.7.0

type JUnitTestCase struct {
	Classname string                `xml:"classname,attr"`
	Name      string                `xml:"name,attr"`
	Time      float64               `xml:"time,attr"`
	Failure   *JUnitTestCaseFailure `xml:"failure"`
	Skipped   *JUnitTestCaseSkipped `xml:"skipped"`
}

type JUnitTestCaseFailure added in v1.7.0

type JUnitTestCaseFailure struct {
	Message string `xml:"message,attr"`
	Type    string `xml:"type,attr"`
}

type JUnitTestCaseSkipped added in v1.7.0

type JUnitTestCaseSkipped struct {
	Message string `xml:"message,attr"`
}

type JUnitTestSuite added in v1.7.0

type JUnitTestSuite struct {
	Tests      int             `xml:"tests,attr"`
	Failures   int             `xml:"failures,attr"`
	Time       float64         `xml:"time,attr"`
	Name       string          `xml:"name,attr"`
	Timestamp  time.Time       `xml:"timestamp,attr"`
	Properties []JUnitProperty `xml:"properties"`
	TestCases  []JUnitTestCase `xml:"testcase"`
}

type Logger

type Logger struct {
	InfoEnabled  bool
	ErrorEnabled bool
	ExitOnFatal  bool
}

Logger can be used for the testing.T parameter for forest functions when you need more control over what to log and how to handle fatals. The variable TestingT is a Logger with all enabled.

func (Logger) Error

func (l Logger) Error(args ...interface{})

Error is equivalent to Log followed by Fail.

func (Logger) Fail

func (l Logger) Fail()

Fail marks the function as having failed but continues execution.

func (Logger) FailNow

func (l Logger) FailNow()

FailNow marks the function as having failed and stops its execution.

func (Logger) Fatal

func (l Logger) Fatal(args ...interface{})

Fatal is equivalent to Log followed by FailNow.

func (Logger) Helper added in v1.4.3

func (l Logger) Helper()

func (Logger) Logf

func (l Logger) Logf(format string, args ...interface{})

Logf formats its arguments according to the format, analogous to Printf, and records the text in the error log. The text will be printed only if the test fails or the -test.v flag is set.

type Map added in v1.3.0

type Map map[string]interface{}

type RequestConfig

type RequestConfig struct {
	URI        string
	BodyReader io.Reader
	HeaderMap  http.Header
	// for Query parameters
	Values         url.Values
	FormData       url.Values
	User, Password string
	Cookies        []*http.Cookie
	// contains filtered or unexported fields
}

RequestConfig holds additional information to construct a Http request.

Example
var cfg *RequestConfig

// set path template and header
cfg = Path("/v1/assets/{id}", "artreyu").
	Header("Accept", "application/json")

// set query parameters (the config will do escaping)
cfg = NewConfig("/v1/assets").
	Query("lang", "en")

// contents as is
cfg = Path("/v1/assets").
	Body("some payload for POST or PUT")

// content from file (io.Reader)
cfg = Path("/v1/assets")
f, _ := os.Open("payload.xml")
cfg.BodyReader = f

// content by marshalling (xml,json,plain text) your value
cfg = NewConfig("/v1/assets").
	Content(time.Now(), "application/json")
Output:

func NewConfig

func NewConfig(pathTemplate string, pathParams ...interface{}) *RequestConfig

NewConfig returns a new RequestConfig with initialized empty headers and query parameters. See Path for an explanation of the function parameters.

func (*RequestConfig) BasicAuth

func (r *RequestConfig) BasicAuth(username, password string) *RequestConfig

BasicAuth sets the credentials for Basic Authentication (if username is not empty)

func (*RequestConfig) Body

func (r *RequestConfig) Body(body string) *RequestConfig

Body sets the playload as is. No content type is set. It sets the BodyReader field of the RequestConfig.

func (*RequestConfig) Build added in v1.5.3

func (r *RequestConfig) Build(method, baseURL string) (*http.Request, error)

Build returns a new HTTP Request.

func (*RequestConfig) Content

func (r *RequestConfig) Content(payload interface{}, contentType string) *RequestConfig

Content encodes (marshals) the payload conform the content type given. If the payload is already a string (JSON,XML,plain) then it is used as is. Supported Content-Type values for marshalling: application/json, application/xml, text/plain Payload can also be a slice of bytes; use application/octet-stream in that case. It sets the BodyReader field of the RequestConfig.

func (*RequestConfig) Cookie added in v1.3.0

func (r *RequestConfig) Cookie(c *http.Cookie) *RequestConfig

Cookie adds a Cookie to the list of cookies to include in the request.

func (*RequestConfig) Do

func (r *RequestConfig) Do(block func(config *RequestConfig)) *RequestConfig

Do calls the one-argument function parameter with the receiver. This allows for custom convenience functions without breaking the fluent programming style.

func (*RequestConfig) Form

func (r *RequestConfig) Form(bodyData url.Values) *RequestConfig

Form set the FormData values e.g for POST operation.

func (*RequestConfig) Header

func (r *RequestConfig) Header(name, value string) *RequestConfig

Header adds a name=value pair to the list of header parameters.

func (*RequestConfig) LogRequestLine added in v1.4.3

func (r *RequestConfig) LogRequestLine(b bool)

LogRequestLine controls whether each HTTP verb call is logging the request line (method + url)

func (*RequestConfig) Path

func (r *RequestConfig) Path(pathTemplate string, pathParams ...interface{}) *RequestConfig

Path sets the URL path with optional path parameters. format example: /v1/persons/{param}/ + 42 => /v1/persons/42 format example: /v1/persons/:param/ + 42 => /v1/persons/42 format example: /v1/assets/*rest + js/some/file.js => /v1/assets/js/some/file.js

func (*RequestConfig) Query

func (r *RequestConfig) Query(name string, value interface{}) *RequestConfig

Query adds a name=value pair to the list of query parameters.

func (*RequestConfig) Read

func (r *RequestConfig) Read(bodyReader io.Reader) *RequestConfig

Read sets the BodyReader for content to send with the request.

type T

type T interface {
	// Logf formats its arguments according to the format, analogous to Printf, and records the text in the error log.
	// The text will be printed only if the test fails or the -test.v flag is set.
	Logf(format string, args ...interface{})
	// Error is equivalent to Log followed by Fail.
	Error(args ...interface{})
	// Fatal is equivalent to Log followed by FailNow.
	Fatal(args ...interface{})
	// FailNow marks the function as having failed and stops its execution.
	FailNow()
	// Fail marks the function as having failed but continues execution.
	Fail()
	Helper()
}

T is the interface that this package is using from standard testing.T

Jump to

Keyboard shortcuts

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