Skip to content

gdt-dev/http

Go Declarative Testing - HTTP

Go Reference Go Report Card Build Status Contributor Covenant

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.

Installation

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

gdt-http test file structure

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 path
  • description: (optional) string with a longer description of the test unit
  • method: (optional) string with the HTTP verb to use. Defaults to "GET" if url attribute is non-empty
  • url: (optional) string with the path or URL to use for the HTTP request. If missing, one of the GET, POST, PATCH, DELETE or PUT shortcut attributes must be non-empty
    • GET: (optional) string with the path or URL to issue an HTTP GET request
    • POST: (optional) string with the path or URL to issue an HTTP POST request
    • PUT: (optional) string with the path or URL to issue an HTTP PUT request
    • PATCH: (optional) string with the path or URL to issue an HTTP PATCH request
    • DELETE: (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 the data structure 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 response
  • strings: (optional) list of strings that should appear in the body of the HTTP response
  • json: (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 body
  • paths: (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 expression
  • path_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 strings
  • schema: (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.

Specify HTTP request payload

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: 201

The 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"
}

Use JSONPath expressions to substitute fixture data

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: 201

Hard-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: 201

The 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"].id

Specify expected response values (assert.json.paths)

When 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 Tolls

Specify expected response value format (assert.json.path_formats)

When 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: uuid

The $.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.

Valid format strings

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

$$LOCATION

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.

Response assertions

Use the assert field in the Spec definition to tell gdt-http to assert that various pieces of the HTTP response match expectations.

Checking for a string in response body

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 parameter

Checking for an HTTP header

Use 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:
       - Accept

TODO(jaypipes): Support Header value matching as well.

Checking for JSON in response

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: uuid4

Validating an HTTP response to a JSONSchema

You 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.json

with 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" ]
}

Contributing and acknowledgements

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.