tdhttp

package
v1.13.0 Latest Latest
Warning

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

Go to latest
Published: Mar 18, 2023 License: BSD-2-Clause Imports: 26 Imported by: 0

Documentation

Overview

Package tdhttp, from go-testdeep, provides some functions to easily test HTTP handlers.

Combined to td package it provides powerful testing features.

TestAPI

The better way to test HTTP APIs using this package.

ta := tdhttp.NewTestAPI(t, mux)

ta.Get("/person/42", "Accept", "application/xml").
  CmpStatus(http.StatusOK).
  CmpHeader(td.ContainsKey("X-Custom-Header")).
  CmpCookie(td.SuperBagOf(td.Smuggle("Name", "cookie_session"))).
  CmpXMLBody(Person{
    ID:   ta.Anchor(td.NotZero(), uint64(0)).(uint64),
    Name: "Bob",
    Age:  26,
  })

ta.Get("/person/42", "Accept", "application/json").
  CmpStatus(http.StatusOK).
  CmpHeader(td.ContainsKey("X-Custom-Header")).
  CmpCookies(td.SuperBagOf(td.Struct(&http.Cookie{Name: "cookie_session"}, nil))).
  CmpJSONBody(td.JSON(`
{
  "id":   $1,
  "name": "Bob",
  "age":  26
}`,
    td.NotZero()))

See the full example below.

Cmp…Response functions

Historically, it was the only way to test HTTP APIs using this package.

ok := tdhttp.CmpJSONResponse(t,
  tdhttp.Get("/person/42"),
  myAPI.ServeHTTP,
  Response{
    Status:  http.StatusOK,
    Header:  td.ContainsKey("X-Custom-Header"),
    Cookies: td.SuperBagOf(td.Smuggle("Name", "cookie_session")),
    Body: Person{
      ID:   42,
      Name: "Bob",
      Age:  26,
    },
  },
  "/person/{id} route")

It now uses TestAPI behind the scene. It is better to directly use TestAPI and its methods instead, as it is more flexible and readable.

Example
package main

import (
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/maxatome/go-testdeep/helpers/tdhttp"
	"github.com/maxatome/go-testdeep/td"
)

func main() {
	t := &testing.T{}

	// Our API handle Persons with 3 routes:
	// - POST /person
	// - GET /person/{personID}
	// - DELETE /person/{personID}

	// Person describes a person.
	type Person struct {
		ID        int64      `json:"id,omitempty" xml:"ID,omitempty"`
		Name      string     `json:"name" xml:"Name"`
		Age       int        `json:"age" xml:"Age"`
		CreatedAt *time.Time `json:"created_at,omitempty" xml:"CreatedAt,omitempty"`
	}

	// Error is returned to the client in case of error.
	type Error struct {
		Mesg string `json:"message" xml:"Message"`
		Code int    `json:"code" xml:"Code"`
	}

	// Our µDB :)
	var mu sync.Mutex
	personByID := map[int64]*Person{}
	personByName := map[string]*Person{}
	var lastID int64

	// reply is a helper to send responses.
	reply := func(w http.ResponseWriter, status int, contentType string, body any) {
		if body == nil {
			w.WriteHeader(status)
			return
		}

		w.Header().Set("Content-Type", contentType)
		w.WriteHeader(status)
		switch contentType {
		case "application/json":
			json.NewEncoder(w).Encode(body) //nolint: errcheck
		case "application/xml":
			xml.NewEncoder(w).Encode(body) //nolint: errcheck
		default: // text/plain
			fmt.Fprintf(w, "%+v", body)
		}
	}

	// Our API
	mux := http.NewServeMux()

	// POST /person
	mux.HandleFunc("/person", func(w http.ResponseWriter, req *http.Request) {
		if req.Method != http.MethodPost {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}

		if req.Body == nil {
			http.Error(w, "Bad request", http.StatusBadRequest)
			return
		}
		defer req.Body.Close()

		var in Person
		var contentType string

		switch req.Header.Get("Content-Type") {
		case "application/json":
			err := json.NewDecoder(req.Body).Decode(&in)
			if err != nil {
				http.Error(w, "Bad request", http.StatusBadRequest)
				return
			}
		case "application/xml":
			err := xml.NewDecoder(req.Body).Decode(&in)
			if err != nil {
				http.Error(w, "Bad request", http.StatusBadRequest)
				return
			}
		case "application/x-www-form-urlencoded":
			b, err := io.ReadAll(req.Body)
			if err != nil {
				http.Error(w, "Bad request", http.StatusBadRequest)
				return
			}
			v, err := url.ParseQuery(string(b))
			if err != nil {
				http.Error(w, "Bad request", http.StatusBadRequest)
				return
			}
			in.Name = v.Get("name")
			in.Age, err = strconv.Atoi(v.Get("age"))
			if err != nil {
				http.Error(w, "Bad request", http.StatusBadRequest)
				return
			}
		default:
			http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
			return
		}

		contentType = req.Header.Get("Accept")

		if in.Name == "" || in.Age <= 0 {
			reply(w, http.StatusBadRequest, contentType, Error{
				Mesg: "Empty name or bad age",
				Code: http.StatusBadRequest,
			})
			return
		}

		mu.Lock()
		defer mu.Unlock()
		if personByName[in.Name] != nil {
			reply(w, http.StatusConflict, contentType, Error{
				Mesg: "Person already exists",
				Code: http.StatusConflict,
			})
			return
		}
		lastID++
		in.ID = lastID
		now := time.Now()
		in.CreatedAt = &now
		personByID[in.ID] = &in
		personByName[in.Name] = &in
		reply(w, http.StatusCreated, contentType, in)
	})

	// GET /person/{id}
	// DELETE /person/{id}
	mux.HandleFunc("/person/", func(w http.ResponseWriter, req *http.Request) {
		id, err := strconv.ParseInt(strings.TrimPrefix(req.URL.Path, "/person/"), 10, 64)
		if err != nil {
			http.Error(w, "Bad request", http.StatusBadRequest)
			return
		}

		accept := req.Header.Get("Accept")

		mu.Lock()
		defer mu.Unlock()
		if personByID[id] == nil {
			reply(w, http.StatusNotFound, accept, Error{
				Mesg: "Person does not exist",
				Code: http.StatusNotFound,
			})
			return
		}

		switch req.Method {
		case http.MethodGet:
			reply(w, http.StatusOK, accept, personByID[id])
		case http.MethodDelete:
			delete(personByID, id)
			reply(w, http.StatusNoContent, "", nil)
		default:
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		}
	})

	//
	// Let's test our API
	//
	ta := tdhttp.NewTestAPI(t, mux)

	// Re-usable custom operator to check Content-Type header
	contentTypeIs := func(ct string) td.TestDeep {
		return td.SuperMapOf(http.Header{"Content-Type": []string{ct}}, nil)
	}

	//
	// Person not found
	//
	ta.Get("/person/42", "Accept", "application/json").
		Name("GET /person/42 - JSON").
		CmpStatus(404).
		CmpHeader(contentTypeIs("application/json")).
		CmpJSONBody(Error{
			Mesg: "Person does not exist",
			Code: 404,
		})
	fmt.Println("GET /person/42 - JSON:", !ta.Failed())

	ta.Get("/person/42", "Accept", "application/xml").
		Name("GET /person/42 - XML").
		CmpStatus(404).
		CmpHeader(contentTypeIs("application/xml")).
		CmpXMLBody(Error{
			Mesg: "Person does not exist",
			Code: 404,
		})
	fmt.Println("GET /person/42 - XML:", !ta.Failed())

	ta.Get("/person/42", "Accept", "text/plain").
		Name("GET /person/42 - raw").
		CmpStatus(404).
		CmpHeader(contentTypeIs("text/plain")).
		CmpBody("{Mesg:Person does not exist Code:404}")
	fmt.Println("GET /person/42 - raw:", !ta.Failed())

	//
	// Create a Person
	//
	var bobID int64
	ta.PostXML("/person", Person{Name: "Bob", Age: 32},
		"Accept", "application/xml").
		Name("POST /person - XML").
		CmpStatus(201).
		CmpHeader(contentTypeIs("application/xml")).
		CmpXMLBody(Person{ // using operator anchoring directly in literal
			ID:        ta.A(td.Catch(&bobID, td.NotZero()), int64(0)).(int64),
			Name:      "Bob",
			Age:       32,
			CreatedAt: ta.A(td.Ptr(td.Between(ta.SentAt(), time.Now()))).(*time.Time),
		})
	fmt.Printf("POST /person - XML: %t → Bob ID=%d\n", !ta.Failed(), bobID)

	var aliceID int64
	ta.PostJSON("/person", Person{Name: "Alice", Age: 35},
		"Accept", "application/json").
		Name("POST /person - JSON").
		CmpStatus(201).
		CmpHeader(contentTypeIs("application/json")).
		CmpJSONBody(td.JSON(` // using JSON operator (yes comment allowed in JSON!)
{
  "id":         $1,
  "name":       "Alice",
  "age":        35,
  "created_at": $2
}`,
			td.Catch(&aliceID, td.NotZero()),
			td.Smuggle(func(date string) (time.Time, error) {
				return time.Parse(time.RFC3339Nano, date)
			}, td.Between(ta.SentAt(), time.Now()))))
	fmt.Printf("POST /person - JSON: %t → Alice ID=%d\n", !ta.Failed(), aliceID)

	var brittID int64
	ta.PostForm("/person",
		url.Values{
			"name": []string{"Britt"},
			"age":  []string{"29"},
		},
		"Accept", "text/plain").
		Name("POST /person - raw").
		CmpStatus(201).
		CmpHeader(contentTypeIs("text/plain")).
		// using Re (= Regexp) operator
		CmpBody(td.Re(`\{ID:(\d+) Name:Britt Age:29 CreatedAt:.*\}\z`,
			td.Smuggle(func(groups []string) (int64, error) {
				return strconv.ParseInt(groups[0], 10, 64)
			}, td.Catch(&brittID, td.NotZero()))))
	fmt.Printf("POST /person - raw: %t → Britt ID=%d\n", !ta.Failed(), brittID)

	//
	// Get a Person
	//
	ta.Get(fmt.Sprintf("/person/%d", aliceID), "Accept", "application/xml").
		Name("GET Alice - XML (ID #%d)", aliceID).
		CmpStatus(200).
		CmpHeader(contentTypeIs("application/xml")).
		CmpXMLBody(td.SStruct( // using SStruct operator
			Person{
				ID:   aliceID,
				Name: "Alice",
				Age:  35,
			},
			td.StructFields{
				"CreatedAt": td.Ptr(td.NotZero()),
			},
		))
	fmt.Println("GET XML Alice:", !ta.Failed())

	ta.Get(fmt.Sprintf("/person/%d", aliceID), "Accept", "application/json").
		Name("GET Alice - JSON (ID #%d)", aliceID).
		CmpStatus(200).
		CmpHeader(contentTypeIs("application/json")).
		CmpJSONBody(td.JSON(` // using JSON operator (yes comment allowed in JSON!)
{
  "id":         $1,
  "name":       "Alice",
  "age":        35,
  "created_at": $2
}`,
			aliceID,
			td.Not(td.Re(`^0001-01-01`)), // time is not 0001-01-01… aka zero time.Time
		))
	fmt.Println("GET JSON Alice:", !ta.Failed())

	//
	// Delete a Person
	//
	ta.Delete(fmt.Sprintf("/person/%d", aliceID), nil).
		Name("DELETE Alice (ID #%d)", aliceID).
		CmpStatus(204).
		CmpHeader(td.Not(td.ContainsKey("Content-Type"))).
		NoBody()
	fmt.Println("DELETE Alice:", !ta.Failed())

	// Check Alice is deleted
	ta.Get(fmt.Sprintf("/person/%d", aliceID), "Accept", "application/json").
		Name("GET (deleted) Alice - JSON (ID #%d)", aliceID).
		CmpStatus(404).
		CmpHeader(contentTypeIs("application/json")).
		CmpJSONBody(td.JSON(`
{
  "message": "Person does not exist",
  "code":    404
}`))
	fmt.Println("Alice is not found anymore:", !ta.Failed())

}
Output:

GET /person/42 - JSON: true
GET /person/42 - XML: true
GET /person/42 - raw: true
POST /person - XML: true → Bob ID=1
POST /person - JSON: true → Alice ID=2
POST /person - raw: true → Britt ID=3
GET XML Alice: true
GET JSON Alice: true
DELETE Alice: true
Alice is not found anymore: true

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func BasicAuthHeader added in v1.10.0

func BasicAuthHeader(user, password string) http.Header

BasicAuthHeader returns a new http.Header with only Authorization key set, compliant with HTTP Basic Authentication using user and password. It is provided as a facility to build request in one line:

ta.Get("/path", tdhttp.BasicAuthHeader("max", "5ecr3T"))

instead of:

req := tdhttp.Get("/path")
req.SetBasicAuth("max", "5ecr3T")
ta.Request(req)

See http.Request.SetBasicAuth for details.

func CmpJSONResponse

func CmpJSONResponse(t testing.TB,
	req *http.Request,
	handler func(w http.ResponseWriter, r *http.Request),
	expectedResp Response,
	args ...any,
) bool

CmpJSONResponse is used to match a JSON response body. req is launched against handler. If expectedResp.Body is non-nil, the response body is json.Unmarshal'ed. The response is then tested against expectedResp.

args... are optional and allow to name the test, a t.Log() done before starting any test. If len(args) > 1 and the first item of args is a string and contains a '%' rune then fmt.Fprintf is used to compose the name, else args are passed to fmt.Fprint.

It returns true if the tests succeed, false otherwise.

ok := tdhttp.CmpJSONResponse(t,
  tdhttp.Get("/person/42"),
  myAPI.ServeHTTP,
  Response{
    Status: http.StatusOK,
    Header: td.ContainsKey("X-Custom-Header"),
    Body:   Person{
      ID:   42,
      Name: "Bob",
      Age:  26,
    },
  },
  "/person/{id} route")

Response.Status, Response.Header and Response.Body fields can all be td.TestDeep operators as it is for Response.Header field here. Otherwise, Response.Status should be an int, Response.Header a http.Header and Response.Body any type one can json.Unmarshal into.

If Response.Status and Response.Header are omitted (or nil), they are not tested.

If Response.Body is omitted (or nil), it means the body response has to be empty. If you want to ignore the body response, use td.Ignore explicitly.

See TestAPI type and its methods for more flexible tests.

func CmpJSONResponseFunc

func CmpJSONResponseFunc(req *http.Request,
	handler func(w http.ResponseWriter, r *http.Request),
	expectedResp Response) func(t *testing.T)

CmpJSONResponseFunc returns a function ready to be used with testing.T.Run, calling CmpJSONResponse behind the scene. As it is intended to be used in conjunction with testing.T.Run which names the sub-test, the test name part (args...) is voluntary omitted.

t.Run("Subtest name", tdhttp.CmpJSONResponseFunc(
  tdhttp.Get("/json"),
  mux.ServeHTTP,
  tdhttp.Response{
    Status: http.StatusOK,
    Body:   JResp{Comment: "expected comment!"},
  }))

See CmpJSONResponse documentation for details.

See TestAPI type and its methods for more flexible tests.

func CmpMarshaledResponse

func CmpMarshaledResponse(t testing.TB,
	req *http.Request,
	handler func(w http.ResponseWriter, r *http.Request),
	unmarshal func([]byte, any) error,
	expectedResp Response,
	args ...any,
) bool

CmpMarshaledResponse is the base function used by some others in tdhttp package. req is launched against handler. The response body is unmarshaled using unmarshal. The response is then tested against expectedResp.

args... are optional and allow to name the test, a t.Log() done before starting any test. If len(args) > 1 and the first item of args is a string and contains a '%' rune then fmt.Fprintf is used to compose the name, else args are passed to fmt.Fprint.

It returns true if the tests succeed, false otherwise.

See TestAPI type and its methods for more flexible tests.

func CmpMarshaledResponseFunc

func CmpMarshaledResponseFunc(req *http.Request,
	handler func(w http.ResponseWriter, r *http.Request),
	unmarshal func([]byte, any) error,
	expectedResp Response) func(t *testing.T)

CmpMarshaledResponseFunc returns a function ready to be used with testing.T.Run, calling CmpMarshaledResponse behind the scene. As it is intended to be used in conjunction with testing.T.Run which names the sub-test, the test name part (args...) is voluntary omitted.

t.Run("Subtest name", tdhttp.CmpMarshaledResponseFunc(
  tdhttp.Get("/text"),
  mux.ServeHTTP,
  tdhttp.Response{
    Status: http.StatusOK,
  }))

See CmpMarshaledResponse for details.

See TestAPI type and its methods for more flexible tests.

func CmpResponse

func CmpResponse(t testing.TB,
	req *http.Request,
	handler func(w http.ResponseWriter, r *http.Request),
	expectedResp Response,
	args ...any) bool

CmpResponse is used to match a []byte or string response body. req is launched against handler. If expectedResp.Body is non-nil, the response body is converted to []byte or string, depending on the expectedResp.Body type. The response is then tested against expectedResp.

args... are optional and allow to name the test, a t.Log() done before starting any test. If len(args) > 1 and the first item of args is a string and contains a '%' rune then fmt.Fprintf is used to compose the name, else args are passed to fmt.Fprint.

It returns true if the tests succeed, false otherwise.

ok := tdhttp.CmpResponse(t,
  tdhttp.Get("/test"),
  myAPI.ServeHTTP,
  Response{
    Status: http.StatusOK,
    Header: td.ContainsKey("X-Custom-Header"),
    Body:   "OK!\n",
  },
  "/test route")

Response.Status, Response.Header and Response.Body fields can all be td.TestDeep operators as it is for Response.Header field here. Otherwise, Response.Status should be an int, Response.Header a http.Header and Response.Body a []byte or a string.

See TestAPI type and its methods for more flexible tests.

func CmpResponseFunc

func CmpResponseFunc(req *http.Request,
	handler func(w http.ResponseWriter, r *http.Request),
	expectedResp Response) func(t *testing.T)

CmpResponseFunc returns a function ready to be used with testing.T.Run, calling CmpResponse behind the scene. As it is intended to be used in conjunction with testing.T.Run which names the sub-test, the test name part (args...) is voluntary omitted.

t.Run("Subtest name", tdhttp.CmpResponseFunc(
  tdhttp.Get("/text"),
  mux.ServeHTTP,
  tdhttp.Response{
    Status: http.StatusOK,
  }))

See CmpResponse documentation for details.

See TestAPI type and its methods for more flexible tests.

func CmpXMLResponse

func CmpXMLResponse(t testing.TB,
	req *http.Request,
	handler func(w http.ResponseWriter, r *http.Request),
	expectedResp Response,
	args ...any,
) bool

CmpXMLResponse is used to match an XML response body. req is launched against handler. If expectedResp.Body is non-nil, the response body is xml.Unmarshal'ed. The response is then tested against expectedResp.

args... are optional and allow to name the test, a t.Log() done before starting any test. If len(args) > 1 and the first item of args is a string and contains a '%' rune then fmt.Fprintf is used to compose the name, else args are passed to fmt.Fprint.

It returns true if the tests succeed, false otherwise.

ok := tdhttp.CmpXMLResponse(t,
  tdhttp.Get("/person/42"),
  myAPI.ServeHTTP,
  Response{
    Status: http.StatusOK,
    Header: td.ContainsKey("X-Custom-Header"),
    Body:   Person{
      ID:   42,
      Name: "Bob",
      Age:  26,
    },
  },
  "/person/{id} route")

Response.Status, Response.Header and Response.Body fields can all be td.TestDeep operators as it is for Response.Header field here. Otherwise, Response.Status should be an int, Response.Header a http.Header and Response.Body any type one can xml.Unmarshal into.

If Response.Status and Response.Header are omitted (or nil), they are not tested.

If Response.Body is omitted (or nil), it means the body response has to be empty. If you want to ignore the body response, use td.Ignore explicitly.

See TestAPI type and its methods for more flexible tests.

func CmpXMLResponseFunc

func CmpXMLResponseFunc(req *http.Request,
	handler func(w http.ResponseWriter, r *http.Request),
	expectedResp Response) func(t *testing.T)

CmpXMLResponseFunc returns a function ready to be used with testing.T.Run, calling CmpXMLResponse behind the scene. As it is intended to be used in conjunction with testing.T.Run which names the sub-test, the test name part (args...) is voluntary omitted.

t.Run("Subtest name", tdhttp.CmpXMLResponseFunc(
  tdhttp.Get("/xml"),
  mux.ServeHTTP,
  tdhttp.Response{
    Status: http.StatusOK,
    Body:   JResp{Comment: "expected comment!"},
  }))

See CmpXMLResponse documentation for details.

See TestAPI type and its methods for more flexible tests.

func Delete added in v1.1.1

func Delete(target string, body io.Reader, headersQueryParams ...any) *http.Request

Delete creates a HTTP DELETE. It is a shortcut for:

tdhttp.NewRequest(http.MethodDelete, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func DeleteJSON added in v1.1.1

func DeleteJSON(target string, body any, headersQueryParams ...any) *http.Request

DeleteJSON creates a HTTP DELETE with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". It is a shortcut for:

tdhttp.NewJSONRequest(http.MethodDelete, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func DeleteXML added in v1.1.1

func DeleteXML(target string, body any, headersQueryParams ...any) *http.Request

DeleteXML creates a HTTP DELETE with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". It is a shortcut for:

tdhttp.NewXMLRequest(http.MethodDelete, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func Get added in v1.1.1

func Get(target string, headersQueryParams ...any) *http.Request

Get creates a new HTTP GET. It is a shortcut for:

tdhttp.NewRequest(http.MethodGet, target, nil, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func Head(target string, headersQueryParams ...any) *http.Request

Head creates a new HTTP HEAD. It is a shortcut for:

tdhttp.NewRequest(http.MethodHead, target, nil, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func NewJSONRequest

func NewJSONRequest(method, target string, body any, headersQueryParams ...any) *http.Request

NewJSONRequest creates a new HTTP request with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". Other headers can be added via headersQueryParams, as in:

req := tdhttp.NewJSONRequest("POST", "/data", body,
  "X-Foo", "Foo-value",
  "X-Zip", "Zip-value",
)

See NewRequest for all possible formats accepted in headersQueryParams.

func NewRequest

func NewRequest(method, target string, body io.Reader, headersQueryParams ...any) *http.Request

NewRequest creates a new HTTP request as httptest.NewRequest does, with the ability to immediately add some headers and/or some query parameters.

Headers can be added using string pairs as in:

req := tdhttp.NewRequest("POST", "/pdf", body,
  "Content-type", "application/pdf",
  "X-Test", "value",
)

or using http.Header as in:

req := tdhttp.NewRequest("POST", "/pdf", body,
  http.Header{"Content-type": []string{"application/pdf"}},
)

or using BasicAuthHeader as in:

req := tdhttp.NewRequest("POST", "/pdf", body,
  tdhttp.BasicAuthHeader("max", "5ecr3T"),
)

or using http.Cookie (pointer or not, behind the scene, http.Request.AddCookie is used) as in:

req := tdhttp.NewRequest("POST", "/pdf", body,
  http.Cookie{Name: "cook1", Value: "val1"},
  &http.Cookie{Name: "cook2", Value: "val2"},
)

Several header sources are combined:

req := tdhttp.NewRequest("POST", "/pdf", body,
  "Content-type", "application/pdf",
  http.Header{"X-Test": []string{"value1"}},
  "X-Test", "value2",
  http.Cookie{Name: "cook1", Value: "val1"},
  tdhttp.BasicAuthHeader("max", "5ecr3T"),
  &http.Cookie{Name: "cook2", Value: "val2"},
)

Produces the following http.Header:

http.Header{
  "Authorization": []string{"Basic bWF4OjVlY3IzVA=="},
  "Content-type":  []string{"application/pdf"},
  "Cookie":        []string{"cook1=val1; cook2=val2"},
  "X-Test":        []string{"value1", "value2"},
}

A string slice or a map can be flatened as well. As NewRequest expects ...any, td.Flatten can help here too:

strHeaders := map[string]string{
  "X-Length": "666",
  "X-Foo":    "bar",
}
req := tdhttp.NewRequest("POST", "/pdf", body, td.Flatten(strHeaders))

Or combined with forms seen above:

req := tdhttp.NewRequest("POST", "/pdf", body,
  "Content-type", "application/pdf",
  http.Header{"X-Test": []string{"value1"}},
  td.Flatten(strHeaders),
  "X-Test", "value2",
  http.Cookie{Name: "cook1", Value: "val1"},
  tdhttp.BasicAuthHeader("max", "5ecr3T"),
  &http.Cookie{Name: "cook2", Value: "val2"},
)

Header keys are always canonicalized using http.CanonicalHeaderKey.

Query parameters can be added using url.Values or more flexible Q, as in:

req := tdhttp.NewRequest("GET", "/pdf",
  url.Values{
    "param": {"val"},
    "names": {"bob", "alice"},
  },
  "X-Test": "a header in the middle",
  tdhttp.Q{
    "limit":   20,
    "ids":     []int64{456, 789},
    "details": true,
  },
)

All url.Values and Q instances are combined to produce the final query string to use. The previous example produces the following target:

/pdf?details=true&ids=456&ids=789&limit=20&names=bob&names=alice&param=val

If target already contains a query string, it is reused:

req := tdhttp.NewRequest("GET", "/pdf?limit=10", tdhttp.Q{"details": true})

produces the following target:

/path?details=true&limit=10

Behind the scene, url.Values.Encode is used, so the parameters are always sorted by key. If you want a specific order, then do not use url.Values nor Q instances, but compose target by yourself.

See Q documentation to learn how values are stringified.

func NewXMLRequest

func NewXMLRequest(method, target string, body any, headersQueryParams ...any) *http.Request

NewXMLRequest creates a new HTTP request with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". Other headers can be added via headersQueryParams, as in:

req := tdhttp.NewXMLRequest("POST", "/data", body,
  "X-Foo", "Foo-value",
  "X-Zip", "Zip-value",
)

See NewRequest for all possible formats accepted in headersQueryParams.

func Options added in v1.12.0

func Options(target string, body io.Reader, headersQueryParams ...any) *http.Request

Options creates a HTTP OPTIONS. It is a shortcut for:

tdhttp.NewRequest(http.MethodOptions, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func Patch added in v1.1.1

func Patch(target string, body io.Reader, headersQueryParams ...any) *http.Request

Patch creates a HTTP PATCH. It is a shortcut for:

tdhttp.NewRequest(http.MethodPatch, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func PatchJSON added in v1.1.1

func PatchJSON(target string, body any, headersQueryParams ...any) *http.Request

PatchJSON creates a HTTP PATCH with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". It is a shortcut for:

tdhttp.NewJSONRequest(http.MethodPatch, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func PatchXML added in v1.1.1

func PatchXML(target string, body any, headersQueryParams ...any) *http.Request

PatchXML creates a HTTP PATCH with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". It is a shortcut for:

tdhttp.NewXMLRequest(http.MethodPatch, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func Post added in v1.1.1

func Post(target string, body io.Reader, headersQueryParams ...any) *http.Request

Post creates a HTTP POST. It is a shortcut for:

tdhttp.NewRequest(http.MethodPost, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func PostForm added in v1.4.0

func PostForm(target string, data URLValuesEncoder, headersQueryParams ...any) *http.Request

PostForm creates a HTTP POST with data's keys and values URL-encoded as the request body. "Content-Type" header is automatically set to "application/x-www-form-urlencoded". Other headers can be added via headersQueryParams, as in:

req := tdhttp.PostForm("/data",
  url.Values{
    "param1": []string{"val1", "val2"},
    "param2": []string{"zip"},
  },
  "X-Foo", "Foo-value",
  "X-Zip", "Zip-value",
)

See NewRequest for all possible formats accepted in headersQueryParams.

func PostJSON added in v1.1.1

func PostJSON(target string, body any, headersQueryParams ...any) *http.Request

PostJSON creates a HTTP POST with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". It is a shortcut for:

tdhttp.NewJSONRequest(http.MethodPost, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func PostMultipartFormData added in v1.10.0

func PostMultipartFormData(target string, data *MultipartBody, headersQueryParams ...any) *http.Request

PostMultipartFormData creates a HTTP POST multipart request, like multipart/form-data one for example. See MultipartBody type for details. "Content-Type" header is automatically set depending on data.MediaType (defaults to "multipart/form-data") and data.Boundary (defaults to "go-testdeep-42"). Other headers can be added via headersQueryParams, as in:

req := tdhttp.PostMultipartFormData("/data",
  &tdhttp.MultipartBody{
    // "multipart/form-data" by default
    Parts: []*tdhttp.MultipartPart{
      tdhttp.NewMultipartPartString("type", "Sales"),
      tdhttp.NewMultipartPartFile("report", "report.json", "application/json"),
    },
  },
  "X-Foo", "Foo-value",
  "X-Zip", "Zip-value",
)

and with a different media type:

req := tdhttp.PostMultipartFormData("/data",
  &tdhttp.MultipartBody{
    MediaType: "multipart/mixed",
    Parts:     []*tdhttp.MultipartPart{
      tdhttp.NewMultipartPartString("type", "Sales"),
      tdhttp.NewMultipartPartFile("report", "report.json", "application/json"),
    },
  },
  "X-Foo", "Foo-value",
  "X-Zip", "Zip-value",
)

See NewRequest for all possible formats accepted in headersQueryParams.

func PostXML added in v1.1.1

func PostXML(target string, body any, headersQueryParams ...any) *http.Request

PostXML creates a HTTP POST with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". It is a shortcut for:

tdhttp.NewXMLRequest(http.MethodPost, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func Put added in v1.1.1

func Put(target string, body io.Reader, headersQueryParams ...any) *http.Request

Put creates a HTTP PUT. It is a shortcut for:

tdhttp.NewRequest(http.MethodPut, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func PutJSON added in v1.1.1

func PutJSON(target string, body any, headersQueryParams ...any) *http.Request

PutJSON creates a HTTP PUT with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". It is a shortcut for:

tdhttp.NewJSONRequest(http.MethodPut, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

func PutXML added in v1.1.1

func PutXML(target string, body any, headersQueryParams ...any) *http.Request

PutXML creates a HTTP PUT with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". It is a shortcut for:

tdhttp.NewXMLRequest(http.MethodPut, target, body, headersQueryParams...)

See NewRequest for all possible formats accepted in headersQueryParams.

Types

type MultipartBody added in v1.10.0

type MultipartBody struct {
	MediaType string           // type to use instead of default "multipart/form-data"
	Boundary  string           // boundary to use between parts. Automatically initialized when calling ContentType().
	Parts     []*MultipartPart // parts composing this multipart/… body.
	// contains filtered or unexported fields
}

MultipartBody is a body of a multipart/form-data HTTP request (by default, or any other multipart/… body, see MediaType field) as defined in RFC 2046 to be used as a io.Reader body of http.Request and so compliant with RFC 2388. It implements io.Reader and can only be read once. See PostMultipartFormData and TestAPI.PostMultipartFormData for examples of use.

func (*MultipartBody) ContentType added in v1.10.0

func (b *MultipartBody) ContentType() string

ContentType returns the Content-Type header to use. As it contains the boundary, it is initialized first if it is still empty. By default the media type is multipart/form-data but it can be overridden using the MediaType field.

m.MediaType = "multipart/mixed"
ct := m.ContentType()

func (*MultipartBody) Read added in v1.10.0

func (b *MultipartBody) Read(p []byte) (n int, err error)

Read implements io.Reader interface.

type MultipartPart added in v1.10.0

type MultipartPart struct {
	Name     string      // is "name" in Content-Disposition. If empty, Content-Disposition header is omitted.
	Filename string      // is optional. If set it is "filename" in Content-Disposition.
	Content  io.Reader   // is the body section of the part.
	Header   http.Header // is the header of the part and is optional. It is automatically initialized when needed.
	// contains filtered or unexported fields
}

MultipartPart is a part in a MultipartBody body. It implements io.Reader and can only be read once.

func NewMultipartPart added in v1.10.0

func NewMultipartPart(name string, body io.Reader, contentType ...string) *MultipartPart

NewMultipartPart returns a new MultipartPart based on body content. If body is nil, it means there is no body at all.

func NewMultipartPartBytes added in v1.10.0

func NewMultipartPartBytes(name string, body []byte, contentType ...string) *MultipartPart

NewMultipartPartBytes returns a new MultipartPart based on body content.

func NewMultipartPartFile added in v1.10.0

func NewMultipartPartFile(name string, filePath string, contentType ...string) *MultipartPart

NewMultipartPartFile returns a new MultipartPart based on filePath content. If filePath cannot be opened, an error is returned on first Read() call.

func NewMultipartPartString added in v1.10.0

func NewMultipartPartString(name string, body string, contentType ...string) *MultipartPart

NewMultipartPartString returns a new MultipartPart based on body content.

func (*MultipartPart) Read added in v1.10.0

func (p *MultipartPart) Read(b []byte) (n int, err error)

Read implements io.Reader interface.

type Q added in v1.10.0

type Q map[string]any

Q allows to easily declare query parameters for use in NewRequest and related http.Request builders, as Get for example:

req := tdhttp.Get("/path", tdhttp.Q{
  "id":     []int64{1234, 4567},
  "dryrun": true,
})

See NewRequest for several examples of use.

Accepted types as values are:

  • fmt.Stringer
  • string
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • float32, float64
  • bool
  • slice or array of any type above, plus any
  • pointer on any type above, plus any or any other pointer

func (Q) AddTo added in v1.10.0

func (q Q) AddTo(qp url.Values) error

AddTo adds the q contents to qp.

func (Q) Encode added in v1.10.0

func (q Q) Encode() string

Encode does the same as url.Values.Encode does. So quoting its doc, it encodes the values into “URL encoded” form ("bar=baz&foo=quux") sorted by key.

It panics if a value cannot be converted.

func (Q) Values added in v1.10.0

func (q Q) Values() url.Values

Values returns a url.Values instance corresponding to q. It panics if a value cannot be converted.

type Response

type Response struct {
	Status  any // is the expected status (ignored if nil)
	Header  any // is the expected header (ignored if nil)
	Cookies any // is the expected cookies (ignored if nil)
	Body    any // is the expected body (expected to be empty if nil)
}

Response is used by Cmp*Response functions to make the HTTP response match easier. Each field, can be a td.TestDeep operator as well as the exact expected value.

type TestAPI added in v1.4.0

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

TestAPI allows to test one HTTP API. See NewTestAPI function to create a new instance and get some examples of use.

func NewTestAPI added in v1.4.0

func NewTestAPI(tb testing.TB, handler http.Handler) *TestAPI

NewTestAPI creates a TestAPI that can be used to test routes of the API behind handler.

tdhttp.NewTestAPI(t, mux).
  Get("/test").
  CmpStatus(200).
  CmpBody("OK!")

Several routes can be tested with the same instance as in:

ta := tdhttp.NewTestAPI(t, mux)

ta.Get("/test").
  CmpStatus(200).
  CmpBody("OK!")

ta.Get("/ping").
  CmpStatus(200).
  CmpBody("pong")

Note that tb can be a *testing.T as well as a *td.T.

func (*TestAPI) A added in v1.4.0

func (ta *TestAPI) A(operator td.TestDeep, model ...any) any

A is a synonym for TestAPI.Anchor. It returns a typed value allowing to anchor the td.TestDeep operator in a go classic literal like a struct, slice, array or map value.

ta := tdhttp.NewTestAPI(tt, mux)

ta.Get("/person/42").
  CmpStatus(http.StatusOK).
  CmpJSONBody(Person{
    ID:   ta.A(td.NotZero(), uint64(0)).(uint64),
    Name: "Bob",
    Age:  26,
  })

See td.T.Anchor for details.

func (*TestAPI) Anchor added in v1.4.0

func (ta *TestAPI) Anchor(operator td.TestDeep, model ...any) any

Anchor returns a typed value allowing to anchor the td.TestDeep operator operator in a go classic literal like a struct, slice, array or map value.

ta := tdhttp.NewTestAPI(tt, mux)

ta.Get("/person/42").
  CmpStatus(http.StatusOK).
  CmpJSONBody(Person{
    ID:   ta.Anchor(td.NotZero(), uint64(0)).(uint64),
    Name: "Bob",
    Age:  26,
  })

See td.T.Anchor for details.

See TestAPI.A method for a shorter synonym of Anchor.

func (*TestAPI) AutoDumpResponse added in v1.9.0

func (ta *TestAPI) AutoDumpResponse(enable ...bool) *TestAPI

AutoDumpResponse allows to dump the HTTP response when the first error is encountered after a request.

ta.AutoDumpResponse()
ta.AutoDumpResponse(true)

both enable the dump.

func (*TestAPI) CmpBody added in v1.4.0

func (ta *TestAPI) CmpBody(expectedBody any) *TestAPI

CmpBody tests the last request response body against expectedBody. expectedBody can be a []byte, a string or a td.TestDeep operator.

ta := tdhttp.NewTestAPI(t, mux)

ta.Get("/test").
  CmpStatus(http.StatusOK).
  CmpBody("OK!\n")

ta.Get("/test").
  CmpStatus(http.StatusOK).
  CmpBody(td.Contains("OK"))

It fails if no request has been sent yet.

func (*TestAPI) CmpCookies added in v1.10.0

func (ta *TestAPI) CmpCookies(expectedCookies any) *TestAPI

CmpCookies tests the last request response cookies against expectedCookies. expectedCookies can be a []*http.Cookie or a td.TestDeep operator. Keep in mind that if it is a []*http.Cookie, it has to match exactly the response cookies. Often only the presence of a cookie key is needed:

ta := tdhttp.NewTestAPI(t, mux).
  PostJSON("/login", map[string]string{"name": "Bob", "password": "Sponge"}).
  CmdStatus(200).
  CmpCookies(td.SuperBagOf(td.Struct(&http.Cookie{Name: "cookie_session"}, nil))).
  CmpCookies(td.SuperBagOf(td.Smuggle("Name", "cookie_session"))) // shorter

To make tests easier, http.Cookie.Raw and http.Cookie.RawExpires fields of each *http.Cookie are zeroed before doing the comparison. So no need to fill them when comparing against a simple literal as in:

ta := tdhttp.NewTestAPI(t, mux).
  PostJSON("/login", map[string]string{"name": "Bob", "password": "Sponge"}).
  CmdStatus(200).
  CmpCookies([]*http.Cookies{
    {Name: "cookieName1", Value: "cookieValue1"},
    {Name: "cookieName2", Value: "cookieValue2"},
  })

It fails if no request has been sent yet.

func (*TestAPI) CmpHeader added in v1.4.0

func (ta *TestAPI) CmpHeader(expectedHeader any) *TestAPI

CmpHeader tests the last request response header against expectedHeader. expectedHeader can be a http.Header or a td.TestDeep operator. Keep in mind that if it is a http.Header, it has to match exactly the response header. Often only the presence of a header key is needed:

ta := tdhttp.NewTestAPI(t, mux).
  PostJSON("/new", map[string]string{"name": "Bob"}).
  CmdStatus(201).
  CmpHeader(td.ContainsKey("X-Custom"))

or some specific key, value pairs:

ta.CmpHeader(td.SuperMapOf(
  http.Header{
    "X-Account": []string{"Bob"},
  },
  td.MapEntries{
    "X-Token": td.Bag(td.Re(`^[a-z0-9-]{32}\z`)),
  }),
)

Note that CmpHeader calls can be chained:

ta.CmpHeader(td.ContainsKey("X-Account")).
  CmpHeader(td.ContainsKey("X-Token"))

instead of doing all tests in one call as td.All operator allows it:

ta.CmpHeader(td.All(
  td.ContainsKey("X-Account"),
  td.ContainsKey("X-Token"),
))

It fails if no request has been sent yet.

func (*TestAPI) CmpJSONBody added in v1.4.0

func (ta *TestAPI) CmpJSONBody(expectedBody any) *TestAPI

CmpJSONBody tests that the last request response body can be json.Unmarshal'ed and that it matches expectedBody. expectedBody can be any type one can json.Unmarshal into, or a td.TestDeep operator.

ta := tdhttp.NewTestAPI(t, mux)

ta.Get("/person/42").
  CmpStatus(http.StatusOK).
  CmpJSONBody(Person{
    ID:   42,
    Name: "Bob",
    Age:  26,
  })

ta.PostJSON("/person", Person{Name: "Bob", Age: 23}).
  CmpStatus(http.StatusCreated).
  CmpJSONBody(td.SStruct(
    Person{
      Name: "Bob",
      Age:  26,
    },
    td.StructFields{
      "ID": td.NotZero(),
    }))

The same with anchoring, and so without td.SStruct:

ta := tdhttp.NewTestAPI(tt, mux)

ta.PostJSON("/person", Person{Name: "Bob", Age: 23}).
  CmpStatus(http.StatusCreated).
  CmpJSONBody(Person{
    ID:   ta.Anchor(td.NotZero(), uint64(0)).(uint64),
    Name: "Bob",
    Age:  26,
  })

The same using td.JSON:

ta.PostJSON("/person", Person{Name: "Bob", Age: 23}).
  CmpStatus(http.StatusCreated).
  CmpJSONBody(td.JSON(`
{
  "id":   NotZero(),
  "name": "Bob",
  "age":  26
}`))

It fails if no request has been sent yet.

func (*TestAPI) CmpMarshaledBody added in v1.4.0

func (ta *TestAPI) CmpMarshaledBody(unmarshal func([]byte, any) error, expectedBody any) *TestAPI

CmpMarshaledBody tests that the last request response body can be unmarshaled using unmarshal function and then, that it matches expectedBody. expectedBody can be any type unmarshal function can handle, or a td.TestDeep operator.

See TestAPI.CmpJSONBody and TestAPI.CmpXMLBody sources for examples of use.

It fails if no request has been sent yet.

func (*TestAPI) CmpResponse added in v1.13.0

func (ta *TestAPI) CmpResponse(expectedResponse any) *TestAPI

CmpResponse tests the last request response status against expectedResponse. expectedResponse can be a *http.Response or more probably a td.TestDeep operator.

ta := tdhttp.NewTestAPI(t, mux)

ta.Get("/test").
  CmpResponse(td.Struct(
    &http.Response{Status: http.StatusOK}, td.StructFields{
      "Header":        td.SuperMapOf(http.Header{"X-Test": {"pipo"}}),
      "ContentLength": td.Gt(10),
    }))

Some tests can be hard to achieve using operators chaining. In this case, the td.Code operator can be used to take the full control over the extractions and comparisons to do:

ta.Get("/test").
  CmpResponse(td.Code(func (assert, require *td.T, r *http.Response) {
    token, err := ParseToken(r.Header.Get("X-Token"))
    require.CmpNoError(err)

    baseURL,err := url.Parse(r.Header.Get("X-Base-URL"))
    require.CmpNoError(err)

    assert.Cmp(baseURL.Query().Get("id"), token.ID)
  }))

It fails if no request has been sent yet.

func (*TestAPI) CmpStatus added in v1.4.0

func (ta *TestAPI) CmpStatus(expectedStatus any) *TestAPI

CmpStatus tests the last request response status against expectedStatus. expectedStatus can be an int to match a fixed HTTP status code, or a td.TestDeep operator.

ta := tdhttp.NewTestAPI(t, mux)

ta.Get("/test").
  CmpStatus(http.StatusOK)

ta.PostJSON("/new", map[string]string{"name": "Bob"}).
  CmpStatus(td.Between(200, 202))

It fails if no request has been sent yet.

func (*TestAPI) CmpTrailer added in v1.13.0

func (ta *TestAPI) CmpTrailer(expectedTrailer any) *TestAPI

CmpTrailer tests the last request response trailer against expectedTrailer. expectedTrailer can be a http.Header or a td.TestDeep operator. Keep in mind that if it is a http.Header, it has to match exactly the response trailer. Often only the presence of a trailer key is needed:

ta := tdhttp.NewTestAPI(t, mux).
  PostJSON("/new", map[string]string{"name": "Bob"}).
  CmdStatus(201).
  CmpTrailer(td.ContainsKey("X-Custom"))

or some specific key, value pairs:

ta.CmpTrailer(td.SuperMapOf(
  http.Header{
    "X-Account": []string{"Bob"},
  },
  td.MapEntries{
    "X-Token": td.Re(`^[a-z0-9-]{32}\z`),
  }),
)

Note that CmpTrailer calls can be chained:

ta.CmpTrailer(td.ContainsKey("X-Account")).
  CmpTrailer(td.ContainsKey("X-Token"))

instead of doing all tests in one call as td.All operator allows it:

ta.CmpTrailer(td.All(
  td.ContainsKey("X-Account"),
  td.ContainsKey("X-Token"),
))

It fails if no request has been sent yet.

Note that until go1.19, it does not handle multiple values in a single Trailer header field.

func (*TestAPI) CmpXMLBody added in v1.4.0

func (ta *TestAPI) CmpXMLBody(expectedBody any) *TestAPI

CmpXMLBody tests that the last request response body can be xml.Unmarshal'ed and that it matches expectedBody. expectedBody can be any type one can xml.Unmarshal into, or a td.TestDeep operator.

ta := tdhttp.NewTestAPI(t, mux)

ta.Get("/person/42").
  CmpStatus(http.StatusOK).
  CmpXMLBody(Person{
    ID:   42,
    Name: "Bob",
    Age:  26,
  })

ta.Get("/person/43").
  CmpStatus(http.StatusOK).
  CmpXMLBody(td.SStruct(
    Person{
      Name: "Bob",
      Age:  26,
    },
    td.StructFields{
      "ID": td.NotZero(),
    }))

The same with anchoring:

ta := tdhttp.NewTestAPI(tt, mux)

ta.Get("/person/42").
  CmpStatus(http.StatusOK).
  CmpXMLBody(Person{
    ID:   ta.Anchor(td.NotZero(), uint64(0)).(uint64),
    Name: "Bob",
    Age:  26,
  })

It fails if no request has been sent yet.

func (*TestAPI) Delete added in v1.4.0

func (ta *TestAPI) Delete(target string, body io.Reader, headersQueryParams ...any) *TestAPI

Delete sends a HTTP DELETE to the tested API. Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) DeleteJSON added in v1.4.0

func (ta *TestAPI) DeleteJSON(target string, body any, headersQueryParams ...any) *TestAPI

DeleteJSON sends a HTTP DELETE with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) DeleteXML added in v1.4.0

func (ta *TestAPI) DeleteXML(target string, body any, headersQueryParams ...any) *TestAPI

DeleteXML sends a HTTP DELETE with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) Failed added in v1.4.0

func (ta *TestAPI) Failed() bool

Failed returns true if any Cmp* or TestAPI.NoBody method failed since last request sending.

func (*TestAPI) Get added in v1.4.0

func (ta *TestAPI) Get(target string, headersQueryParams ...any) *TestAPI

Get sends a HTTP GET to the tested API. Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) Head added in v1.4.0

func (ta *TestAPI) Head(target string, headersQueryParams ...any) *TestAPI

Head sends a HTTP HEAD to the tested API. Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) Name added in v1.4.0

func (ta *TestAPI) Name(args ...any) *TestAPI

Name allows to name the series of tests that follow. This name is used as a prefix for all following tests, in case of failure to qualify each test. If len(args) > 1 and the first item of args is a string and contains a '%' rune then fmt.Fprintf is used to compose the name, else args are passed to fmt.Fprint.

func (*TestAPI) NewJSONRequest added in v1.4.0

func (ta *TestAPI) NewJSONRequest(method, target string, body any, headersQueryParams ...any) *TestAPI

NewJSONRequest sends a HTTP request with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) NewXMLRequest added in v1.4.0

func (ta *TestAPI) NewXMLRequest(method, target string, body any, headersQueryParams ...any) *TestAPI

NewXMLRequest sends a HTTP request with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) NoBody added in v1.4.0

func (ta *TestAPI) NoBody() *TestAPI

NoBody tests that the last request response body is empty.

It fails if no request has been sent yet.

func (*TestAPI) Options added in v1.12.0

func (ta *TestAPI) Options(target string, body io.Reader, headersQueryParams ...any) *TestAPI

Options sends a HTTP OPTIONS to the tested API. Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) Or added in v1.7.0

func (ta *TestAPI) Or(fn any) *TestAPI

Or executes function fn if ta.Failed() is true at the moment it is called.

fn can have several types:

  • func(body string) or func(t *td.T, body string) → fn is called with response body as a string. If no response has been received yet, body is "";
  • func(body []byte) or func(t *td.T, body []byte) → fn is called with response body as a []byte. If no response has been received yet, body is nil;
  • func(t *td.T, resp *httptest.ResponseRecorder) → fn is called with the internal object containing the response. See net/http/httptest for details. If no response has been received yet, resp is nil.

If fn type is not one of these types, it calls ta.T().Fatal().

func (*TestAPI) OrDumpResponse added in v1.9.0

func (ta *TestAPI) OrDumpResponse() *TestAPI

OrDumpResponse dumps the response if at least one previous test failed.

ta := tdhttp.NewTestAPI(t, handler)

ta.Get("/foo").
  CmpStatus(200).
  OrDumpResponse(). // if status check failed, dumps the response
  CmpBody("bar")    // if it fails, the response is not dumped

ta.Get("/foo").
  CmpStatus(200).
  CmpBody("bar").
  OrDumpResponse() // dumps the response if status and/or body checks fail

See TestAPI.AutoDumpResponse method to automatize this dump.

func (*TestAPI) Patch added in v1.4.0

func (ta *TestAPI) Patch(target string, body io.Reader, headersQueryParams ...any) *TestAPI

Patch sends a HTTP PATCH to the tested API. Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) PatchJSON added in v1.4.0

func (ta *TestAPI) PatchJSON(target string, body any, headersQueryParams ...any) *TestAPI

PatchJSON sends a HTTP PATCH with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) PatchXML added in v1.4.0

func (ta *TestAPI) PatchXML(target string, body any, headersQueryParams ...any) *TestAPI

PatchXML sends a HTTP PATCH with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) Post added in v1.4.0

func (ta *TestAPI) Post(target string, body io.Reader, headersQueryParams ...any) *TestAPI

Post sends a HTTP POST to the tested API. Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) PostForm added in v1.4.0

func (ta *TestAPI) PostForm(target string, data URLValuesEncoder, headersQueryParams ...any) *TestAPI

PostForm sends a HTTP POST with data's keys and values URL-encoded as the request body to the tested API. "Content-Type" header is automatically set to "application/x-www-form-urlencoded". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) PostJSON added in v1.4.0

func (ta *TestAPI) PostJSON(target string, body any, headersQueryParams ...any) *TestAPI

PostJSON sends a HTTP POST with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) PostMultipartFormData added in v1.10.0

func (ta *TestAPI) PostMultipartFormData(target string, data *MultipartBody, headersQueryParams ...any) *TestAPI

PostMultipartFormData sends a HTTP POST multipart request, like multipart/form-data one for example. See MultipartBody type for details. "Content-Type" header is automatically set depending on data.MediaType (defaults to "multipart/form-data") and data.Boundary (defaults to "go-testdeep-42"). Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

ta.PostMultipartFormData("/data",
  &tdhttp.MultipartBody{
    // "multipart/form-data" by default
    Parts: []*tdhttp.MultipartPart{
      tdhttp.NewMultipartPartString("type", "Sales"),
      tdhttp.NewMultipartPartFile("report", "report.json", "application/json"),
    },
  },
  "X-Foo", "Foo-value",
  "X-Zip", "Zip-value",
)

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) PostXML added in v1.4.0

func (ta *TestAPI) PostXML(target string, body any, headersQueryParams ...any) *TestAPI

PostXML sends a HTTP POST with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) Put added in v1.4.0

func (ta *TestAPI) Put(target string, body io.Reader, headersQueryParams ...any) *TestAPI

Put sends a HTTP PUT to the tested API. Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) PutJSON added in v1.4.0

func (ta *TestAPI) PutJSON(target string, body any, headersQueryParams ...any) *TestAPI

PutJSON sends a HTTP PUT with body marshaled to JSON. "Content-Type" header is automatically set to "application/json". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) PutXML added in v1.4.0

func (ta *TestAPI) PutXML(target string, body any, headersQueryParams ...any) *TestAPI

PutXML sends a HTTP PUT with body marshaled to XML. "Content-Type" header is automatically set to "application/xml". Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

See NewRequest for all possible formats accepted in headersQueryParams.

func (*TestAPI) Request added in v1.4.0

func (ta *TestAPI) Request(req *http.Request) *TestAPI

Request sends a new HTTP request to the tested API. Any Cmp* or TestAPI.NoBody methods can now be called.

Note that TestAPI.Failed status is reset just after this call.

func (*TestAPI) Run added in v1.9.0

func (ta *TestAPI) Run(name string, f func(ta *TestAPI)) bool

Run runs f as a subtest of t called name.

func (*TestAPI) SentAt added in v1.4.0

func (ta *TestAPI) SentAt() time.Time

SentAt returns the time just before the last request is handled. It can be used to check the time a route sets and returns, as in:

ta.PostJSON("/person/42", Person{Name: "Bob", Age: 23}).
  CmpStatus(http.StatusCreated).
  CmpJSONBody(Person{
    ID:        ta.A(td.NotZero(), uint64(0)).(uint64),
    Name:      "Bob",
    Age:       23,
    CreatedAt: ta.A(td.Between(ta.SentAt(), time.Now())).(time.Time),
  })

checks that CreatedAt field is included between the time when the request has been sent, and the time when the comparison occurs.

func (*TestAPI) T added in v1.9.0

func (ta *TestAPI) T() *td.T

T returns the internal instance of *td.T.

func (*TestAPI) With added in v1.9.0

func (ta *TestAPI) With(tb testing.TB) *TestAPI

With creates a new *TestAPI instance copied from t, but resetting the testing.TB instance the tests are based on to tb. The returned instance is independent from t, sharing only the same handler.

It is typically used when the TestAPI instance is “reused” in sub-tests, as in:

func TestMyAPI(t *testing.T) {
  ta := tdhttp.NewTestAPI(t, MyAPIHandler())

  ta.Get("/test").CmpStatus(200)

  t.Run("errors", func (t *testing.T) {
    ta := ta.With(t)

    ta.Get("/test?bad=1").CmpStatus(400)
    ta.Get("/test?bad=buzz").CmpStatus(400)
  }

  ta.Get("/next").CmpStatus(200)
}

Note that tb can be a *testing.T as well as a *td.T.

See TestAPI.Run for another way to handle subtests.

type URLValuesEncoder added in v1.13.0

type URLValuesEncoder interface {
	Encode() string
}

URLValuesEncoder is an interface PostForm and TestAPI.PostForm data must implement. Encode can be called to generate a "URL encoded" form such as ("bar=baz&foo=quux") sorted by key.

url.Values and Q implement this interface.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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