Skip to content

Hassle-free REST API testing for Go

License

Notifications You must be signed in to change notification settings

jefflinse/melatonin

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

melatonin

Build Status GitHub tag (latest SemVer) GitHub go.mod Go version Go Report Card Go Reference License

Melatonin is a flexible API testing library for Go.

It provides syntactic sugar for writing table-based API tests at any level of testing.

Use it to write:

  • Native Go tests that test your http.Handlers routes directly. Mock out your dependencies and test your handler logic in isolation. More »

  • Component tests that target any running service. Spin up your service with stubbed dependencies and test the API surface. More »

  • E2E test suites that target APIs across multiple running services. Perform acceptance tests against your entire system. More »

See the full user guide and the API documentation for more information.

Installation

go get github.com/jefflinse/melatonin/mt

Usage

Native Go tests

A HandlerContext wraps a Go http.Handler (such as a mux/router) and provides methods for defining tests that run against the it. This is useful, for example, for testing the logic of your mux and individual handlers in isolation with mocked dependencies.

func TestMyAPI(t *testing.T) {
    // myHandler can be anything implementing http.Handler
    myAPI := mt.NewHandlerContext(myHandler)
    mt.RunTestsT(t, []mt.TestCase{

        myAPI.GET("/resource", "Fetch a resource successfully").
            ExpectStatus(200).
            ExpectBody("Hello, world!"),
    })
}

Run these tests with go test, just like any other Go tests.

Component tests

A URLContext wraps a base URL and provides methods for defining tests that run against the API at that URL. This is useful for blackbox testing the API surface of a service, either with real or stubbed external dependencies.

func main() {
    // myURL can be any valid base URL parsable by url.Parse()
    myAPI := mt.NewURLContext(myURL)
    results := mt.RunTests([]mt.TestCase{

        myAPI.GET("/resource", "Fetch a resource successfully").
            ExpectStatus(200).
            ExpectBody("Hello, world!"),
    })

    mt.PrintResults(results)
}

E2E test suites

Similar to component tests, it's easy to create multiple test contexts (i.e. one per service) and define test suites that execute high-level user stories across your entire system.

func main() {
    authAPI := mt.NewURLContext("https://myapi.example.com/auth")
    usersAPI := mt.NewURLContext("https://myapi.example.com/users")

    var uid, token string

    results := mt.RunTests([]mt.TestCase{

        authAPI.POST("/login", "Can log in").
            WithBody(json.Object{
                "username": "someone@example.com",
                "password": "password",
            }).
            ExpectStatus(200).
            ExpectBody(json.Object{
                "uid":           bind.String(&uid)
                "access_token":  bind.String(&token),
                "refresh_token": expect.String(),
            }),

        usersAPI.GET("/:id/profile}", "Can fetch own profile").
            WithHeader("Authorization", "Bearer " + &token).
            WithPathParam("id", &uid).
            ExpectStatus(200).
            ExpectBody("Hello, world!"),
    })

    mt.PrintResults(results)
}

More on data binding and expectations can be found in the user guide.

Examples

See the examples directory for full, runnable examples.

Test a Go HTTP handler

myAPI := mt.NewHandlerContext(http.NewServeMux())
mt.RunTests(...)

Test a base URL endpoint

myAPI := mt.NewURLContext("http://example.com")
mt.RunTests(...)

Define tests

myAPI := mt.NewURLContext("http://example.com")
tests := []mt.TestCase{

    myAPI.GET("/resource").
       ExpectStatus(200).
       ExpectBody(String("Hello, World!")),
    
    myAPI.POST("/resource").
       WithBody(Object{
         "name": "Burt Macklin",
         "age":  32,
       }).
       ExpectStatus(201),
    
    myAPI.DELETE("/resource/42").
       ExpectStatus(204),
}

Use a custom HTTP client for requests

client := &http.Client{}
myAPI := mt.NewURLContext("http://example.com").WithHTTPClient(client)

Use a custom timeout for all tests

timeout := time.Duration(5 * time.Second)
myAPI := mt.NewURLContext("http://example.com").WithTimeout(timeout)

Specify a timeout for a specific test

myAPI.GET("/resource").
    WithTimeout(5 * time.Second).
    ExpectStatus(200).

Specify query parameters for a test

Inline:

myAPI.GET("/resource?first=foo&second=bar")

Individually:

myAPI.GET("/resource").
    WithQueryParam("first", "foo").
    WithQueryParam("second", "bar")

All At Once:

myAPI.GET("/resource").
    WithQueryParams(url.Values{
        "first": []string{"foo"},
        "second": []string{"bar"},
    })

Allow or disallow further tests to run after a failure

runner := mt.NewURLContext("http://example.com").WithContinueOnFailure(true)

Create a test case with a custom HTTP request

req, err := http.NewRequest("GET", "http://example.com/resource", nil)
myAPI.DO(req).
    ExpectStatus(200)

Expect exact headers and JSON body content

Any unexpected headers or JSON keys or values present in the response will cause the test case to fail.

myAPI.GET("/resource").
    ExpectExactHeaders(http.Header{
        "Content-Type": []string{"application/json"},
    }).
    ExpectExactBody(mt.Object{
        "foo": "bar",
    })

Load expectations for a test case from a golden file

myAPI.GET("/resource").
    ExpectGolden("path/to/file.golden")

Golden files keep your test definitions short and concise by storing expectations in a file. See the golden file format specification.

Planned Features

  • Output test results in different formats (e.g. JSON, XML, YAML)
  • Generate test cases from an OpenAPI specification
  • Support for testing GraphQL APIs
  • Support for testing gRPC APIs
  • Support for testing websockets

See the full V1 milestone for more.

Contributing

Please open an issue if you find a bug or have a feature request.

License

MIT License (MIT) - see LICENSE for details.