gdt is a testing library that allows test authors to cleanly describe tests
in a YAML file. gdt reads YAML files that describe a test's assertions and
then builds a set of Go structures that the standard Go
testing package can execute.
This github.com/gdt-dev/http (shortened hereafter to gdt-http) repository
is a companion Go library for gdt that allows test authors to cleanly
describe functional tests of HTTP APIs using a simple, clear YAML format.
gdt-http parses YAML files that describe HTTP requests and assertions about
what the HTTP response should contain.
gdt-http is a Golang library and is intended to be included in your own Golang
application's test code as a Golang package dependency.
Install gdt-http into your $GOPATH by executing:
go get -u github.com/gdt-dev/http
The gdt-http test file parser parses a test file with type
"http". It parses the test file into an object with the following attributes:
tests: list of test unit objects that describe a test of an HTTP request and response
Each of the test unit objects have the following attributes:
name: (optional) string describing the individual test. If missing or empty, the test unit's name is a string with the request HTTP method and pathdescription: (optional) string with a longer description of the test unitmethod: (optional) string with the HTTP verb to use. Defaults to "GET" ifurlattribute is non-emptyurl: (optional) string with the path or URL to use for the HTTP request. If missing, one of theGET,POST,PATCH,DELETEorPUTshortcut attributes must be non-emptyGET: (optional) string with the path or URL to issue an HTTP GET requestPOST: (optional) string with the path or URL to issue an HTTP POST requestPUT: (optional) string with the path or URL to issue an HTTP PUT requestPATCH: (optional) string with the path or URL to issue an HTTP PATCH requestDELETE: (optional) string with the path or URL to issue an HTTP DELETE request
data: (optional) if present, will be encoded into the HTTP request payload. Elements of thedatastructure may be JSONPath expressions (see below)assert: (optional) object describing the assertions to make about the HTTP response received after issuing the HTTP request
The asssert object has the following attributes:
status: (optional) integer corresponding to the expected HTTP status code of the HTTP responsestrings: (optional) list of strings that should appear in the body of the HTTP responsejson: (optional) object describing the assertions to make about JSON content in the HTTP response body
The json object has the following attributes:
len: (optional) integer representing the number of bytes in the resulting JSON object after successfully parsing the HTTP response bodypaths: (optional) map of strings where the keys of the map are JSONPath expressions and the values of the map are the expected value to be found when evaluating the JSONPath expressionpath_formats: (optional) map of strings where the keys of the map are JSONPath expressions and the values of the map are the expected format of the value to be found when evaluating the JSONPath expression. See the list of valid format stringsschema: (optional) string containing a filepath to a JSONSchema document. If present, the JSON included in the HTTP response will be validated against this JSONSChema document.
The data attribute of the test unit is used to specify a payload to be
encoded into the HTTP request body. By default, the contents of the data
attribute are encoded as JSON.
TODO(jaypipes): Support non-JSON encoding.
The data attribute is especially useful for testing of POST and PUT
requests, where you want to send data to the server to create or update some
resource.
For example, suppose the POST /books URL accepts some JSON-encoded data with
information about the to-be-created book's author, title, publisher, etc.
To test the POST /books functionality, a test author might use the following
test unit:
- name: create a new book
POST: /books
data:
title: For Whom The Bell Tolls
published_on: 1940-10-21
pages: 480
author_id: "1"
publisher_id: "1"
assert:
status: 201The above data would be encoded into the following HTTP request body:
{
"title": "For Whom The Bell Tolls",
"published_on": "1940-10-21",
"pages": 480,
"author_id": "1",
"publisher_id": "1"
}Often, you will want to reference some information in a fixture instead of
hard-coding values in the data contents.
Consider this test unit:
- name: create a new book
POST: /books
data:
title: For Whom The Bell Tolls
published_on: 1940-10-21
pages: 480
author_id: "1"
publisher_id: "1"
assert:
status: 201Hard-coding the string "1" for the author_id and publisher_id values is
fragile and non-descriptive. It requires the reader of the test to know which
author has an ID of "1" and which publisher has an ID of "1".
It would be much more readable if we could replace those hard-coded "1"
values with a reference to some fixture data:
fixtures:
- books_api
- books_data
tests:
- name: create a new book
POST: /books
data:
title: For Whom The Bell Tolls
published_on: 1940-10-21
pages: 480
author_id: $.authors.by_name["Ernest Hemingway"].id
publisher_id: $.publishers.by_name["Charles Scribner's Sons"].id
assert:
status: 201The test reader can now better understand what value is being placed into the "author_id" field of the HTTP request payload: the ID value of the author whose name is "Ernest Hemingway".
The JSONPath expressions that replaced the hard-coded "1" values are
evaluated by the fixtures associated with a test file. The
A gdt.fixtures.JSONFixture fixture is designed to evaluate JSONPath
expressions for the data defined in the fixture.
Assume I have a file testdata/fixtures.json that looks like this:
{
"books": [
{
"id": "12ac1b94-5667-461e-80cb-ba8619cae61a",
"title": "Old Man and the Sea",
"published_on": "1952-10-01",
"pages": 127,
"author": {
"name": "Ernest Hemingway",
"id": "1"
},
"publisher": {
"name": "Charles Scribner's Sons",
"id": "1",
"address": {
"address": "153–157 Fifth Avenue",
"city": "New York City",
"state": "NY",
"postal_code": "10010",
"country_code": "US"
}
}
}
],
"authors": {
"by_name": {
"Ernest Hemingway": {
"id": 1
}
}
},
"publishers": {
"by_name": {
"Charles Scribner's Sons": {
"id": 1
}
}
}
}We can register a gdt.fixtures.JSONFixture that contains the data in
testdata/fixtures.json:
dataFilepath := "testdata/fixtures.json"
dataFile, _ := os.Open(dataFilepath)
dataFixture, err := gdt.NewJSONFixture(dataFile)
if err != nil {
panic(err)
}
gdt.RegisterFixture("books_data", dataFixture)To reference any of the data in your gdt.fixtures.JSONFixture from your test
unit, just make sure the fixture is listed in the test file's fixtures field:
require:
- books_data
Then you can grab any data in the fixture using a JSONPath expression in the
data contents:
data:
author_id: $.authors.by_name["Ernest Hemingway"].idWhen you want to validate the structure of the returned JSON object in an HTTP
response body, you use the assert.json.paths attribute of the test unit.
This attribute is a map of string to string, where the map keys are JSONPath expressions and the map values are the expected value when evaluating that JSONPath expression.
For example, let's say you want to verify that an HTTP GET request to the
/books URL returns an HTTP response that contains a list of JSON objects, and
that the first JSON object in that list contains a field, "title", that
contains the string "For Whom the Bell Tolls". You would write the test unit
like so:
tests:
- GET: /books
assert:
json:
paths:
- $[0].title: For Whom the Bell TollsWhen you want to validate that a certain field in a returned JSON object from
an HTTP response matches a particular common format, you use the
assert.json.path_formats attribute of the test unit.
This attribute is a map of string to string, where the map keys are JSONPath expressions and the map values are the type of format that the value to be found at the JSONPath expression should have.
For example, let's say you want to verify that an HTTP GET request to the
/books/thebook URL returned an HTTP response that contains a JSON object
having a "id" field, and that the value of that field is a valid version 4
UUID. You would write the test unit like so:
tests:
- GET: /books/thebook
assert:
json:
path_formats:
- $.id: uuidThe $.id string is a JSONPath expression that selects the value of the field
called "id" from the top-level document/object. The uuid4 string indicates
the expected format of that value.
The currently supported format strings are all format strings in Draft7 of JSONSchema plus the "uuid4" variant:
-
"date": must be a date string in the format YYYY-MM-DD
-
"time": must be a time string in format HH:MM:SSZ-07:00 or HH:MM:SS
-
"date-time": must be a date-time string in any of the following formats:
-
YYYY-MM-DDTHH:MM:SSZ-0700
-
YYYY-MM-DD
-
HH:MM:SSZ-0700
-
HH:MM:SS
-
"hostname": must be a valid DNS hostname (RFC 952 and RFC 1123)
-
"email": must be a valid email address (RFC 5322)
-
"idn-email": must be a valid email address (RFC 5322)
-
"ipv4": must be a valid IPv4 address (RFC 791)
-
"ipv6": must be a valid IPv6 address (RFC 4291)
-
"uri": must be a valid URI (RFC 3986)
-
"uri-reference": must be a valid URI or relative-reference (RFC 3986)
-
"iri": must be a valid URI (RFC 3986)
-
"iri-reference": must be a valid URI or relative-reference (RFC 3986)
-
"uri-template": must be a valid URI template (RFC 6570)
-
"regex": must be a valid POSIX regular expression
-
"json-pointer": must be a valid JSON pointer value
-
"relative-json-pointer": must be a valid relative JSON pointer value
-
"uuid": must be any version of UUID
-
"uuid4": must be a UUID version 4
The url attribute of an HTTP test spec can be the special string
$$LOCATION. When this is set, the HTTP request will be to the URL specified
in the previous HTTP response's Location HTTP header. This is an easy
shortcut for testing a series of ordered HTTP requests, where the first HTTP
request (typically a POST or PUT to a particular resource) responds with a
Location HTTP header pointing to a URL that can have issued an HTTP GET
request to return information about the previously created or mutated resource.
Use the assert field in the Spec definition to tell gdt-http to assert
that various pieces of the HTTP response match expectations.
Use the assert.strings field to check for the existence of one of more
strings in the HTTP response body.
fixtures:
- books_api
tests:
- name: invalid query parameter is supplied
GET: /books?invalidparam=1
assert:
status: 400
strings:
- invalid parameterUse the assert.headers field to check for the existence of one of
more HTTP headers in the HTTP response.
fixtures:
- books_api
tests:
- name: invalid query parameter is supplied
GET: /books?invalidparam=1
assert:
status: 400
headers:
- AcceptTODO(jaypipes): Support Header value matching as well.
Use the assert.json field to assert that the value or format of a value of
an element identified by JSONPath expression matches an expected value or
format.
examples/books/tests/api/create_then_get.yaml:
fixtures:
- books_api
tests:
- name: create a new book
POST: /books
data:
title: For Whom The Bell Tolls
published_on: 1940-10-21
pages: 480
author_id: $.authors.by_name["Ernest Hemingway"].id
publisher_id: $.publishers.by_name["Charles Scribner's Sons"].id
assert:
status: 201
headers:
- Location
- name: look up that created book
GET: $$LOCATION
assert:
status: 200
json:
paths:
$.author.name: Ernest Hemingway
$.publisher.address.state: NY
path_formats:
$.id: uuid4You can use the assert.json.schema field to specify a JSONSchema that the
HTTP response body should adhere to.
examples/books/tests/api/get_books.yaml:
fixtures:
- books_api
- books_data
tests:
- name: list all books
GET: /books
assert:
status: 200
json:
schema: schemas/get_books.jsonwith the contents of examples/books/tests/api/schemas/get_books.json:
{
"$id": "/schemas/get_books.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"books": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"pages": {
"type": "number"
},
"author": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"require": [ "id", "name" ]
},
"publisher": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"require": [ "id", "name" ]
}
},
"required": [ "id", "title", "author" ]
}
}
},
"required": [ "books" ]
}gdt was inspired by Gabbi, the excellent
Python declarative testing framework. gdt tries to bring the same clear,
concise test definitions to the world of Go functional testing.
The Go gopher logo, from which gdt's logo was derived, was created by Renee French.
Contributions to gdt-http are welcomed! Feel free to open a Github issue or
submit a pull request.
