Cro::HTTP::Test
Tests for a Cro HTTP service can in principle be written by hosting the application using Cro::HTTP::Server, using Cro::HTTP::Client to make requests to it, and using the standard Test
library to check the results. This library makes writing such tests easier, and executing them faster, by:
Providing a more convenient API for making test requests and checking the results
Skipping the network and just passing
Cro::TCP
objects from the client pipeline to the server pipeline and vice versa
A basic example§
Given a module MyService::Routes
that looks like:
sub routes() is export {
route {
get -> {
content 'text/plain', 'Nothing to see here';
}
post -> 'add' {
request-body 'application-json' => -> (:$x!, :$y!) {
content 'application/json', { :result($x + $y) };
}
}
}
}
We could write tests for it like this:
use Cro::HTTP::Test;
use MyService::Routes;
test-service routes(), {
test get('/'),
status => 200,
content-type => 'text/plain',
body => /nothing/;
test-given '/add', {
test post(json => { :x(37), :y(5) }),
status => 200,
json => { :result(42) };
test post(json => { :x(37) }),
status => 400;
test get(json => { :x(37) }),
status => 405;
}
}
done-testing;
Setting the service to test§
The test-service
function has two candidates.
The test-service(Cro::Transform, &tests, :$fake-auth, :$http)
candidate runs the tests against the HTTP application provided, which can be any Cro::Transform
that consumes a Cro::HTTP::Request and produces a Cro::HTTP::Response. Applications that are written with Cro::HTTP::Router do this. It is also possible to use Cro.compose
to put (potentially mock) middleware in place also. The optional :$fake-auth
parameter, if passed, will prepend a middleware that sets the auth
of the request to the specified object. This is useful for simulating a user or session and thus testing authorization. The http
argument specifies the HTTP version to run the tests under. Since we control both client and server side in the test, a setting of :http<1.1 2>
is not allowed. The default is :http<2>
. It is also possible to fake the peer-host
and peer-port
by passing these named arguments to test-service
.
The test-service($uri, &tests)
candidate runs the tests against the specified base URI, connecting to it through Cro::HTTP::Client. This makes it possible to use Cro::HTTP::Test to write tests for services built using something other than Cro.
All other named parameters are passed as Cro::HTTP::Client constructor arguments.
Writing tests§
The test
function is for use inside of the block passed to test-service
. It expects to be passed one positional argument representing the request to test, and named parameters indicating the expected properties of the response.
The request is specified by calling one of get
, put
, post
, delete
, head
, or patch
. There's also request($method, ...)
for other HTTP methods (in fact, get
will just call request('GET', ...)
). These functions accept an optional positional parameter providing a relative URI, which if provided will be appended to the current effective base URI. The :$json
named parameter is treated specially, expanding to { content-type => 'application/json', body => $json)
. All other named parameters will be passed on to the Cro::HTTP::Client request
method, thus making all of the HTTP client's functionality available.
Named parameters to the test
function constitute checks. They largely follow the names of methods on the Cro::HTTP::Response object. The available checks are as follows.
status§
Smartmatches the status
property of the response against the check. While an integer, such as status => 200
, will be most common, it is also possible to so things like status => * < 400
(e.g. not an error).
content-type§
Checks the content-type is equivalent. If passed a string, it parses it as a media type and checks the type and subtype match that of the response. If there are any extra parameters in the string (such as a charset), then these will be checked for in the received media type also. If the received media type has extra parameters that are not mentioned, then these will be disregarded. Thus a check content-type => 'text/plain'
matches text/plain; charset=utf8
in the response.
For more fine-grained control, pass a block, which will be passed an instance of Cro::MediaType and expected to return someting truthy for the test to pass.
header or headers§
Takes either a hash mapping header names to header values, or a list of Pair
doing the same. The test passes if the headers are present and the value of the header smartmatches against the value. Use *
when only caring that the header exists, but not wishing to check its values. All other headers in the response will be ignored (that is, extra headers are considered fine).
headers => {
Strict-Transport-Security => *,
Cache-Control => /public/
}
For further control, pass a block, which will receive a List
of Pair
, each one representing a header. Its return value should the truthy for the test to pass.
body-text§
Obtains the body-text
of the response and smart-matches it against the provided value. A string, regex, or code object are all potentially useful.
body-text => /:i success/
The body test will be skipped if there is a content-type
tested and that test fails.
body-blob§
Obtains the body-blob
of the response and smart-matches it against the provided value.
body-blob => *.bytes > 128
The body test will be skipped if there is a content-type
tested and that test fails.
body§
Obtains the body
of the response and smart-matches it against the provided value. Note that the body
property decides what to produce based on the content-type
of the response, thus picking an appropriate body parser. It is thus recommended to use this together with content-type
(that will always be tested ahead of body
, and the body
test skipped if it fails).
json§
This is a convenience short-cut for the common case of a JSON response. It imples content-type => { .type eq 'application' && .subtype-name eq 'json' || .suffix eq 'json' }
(that is, it accepts application/json
or something like application/vnd.foobar+json
).
If passed a code value, then the code will be invoked with the deserialized JSON body and should return a truthy value for the test to pass. Otherwise, the is-deeply
test routine will be used to check the structure of the JSON that was received matches what was expected.
Many tests with one URI, set of headers, etc.§
It can get tedious to repeat the same details of a test. For example, it is common to wish to write many tests against the same URI, passing it a different body or using different request methods each time. The test-given
function comes in various forms. It can be used with a URI and a block:
test-given '/add', {
test post(json => { :x(37), :y(5) }),
status => 200,
json => { :result(42) };
test post(json => { :x(37) }),
status => 400;
}
In this case, the tests will all be performed against this URI appended to the current effective URI, which in a test-service
block is the base URI of the service being tested. If the individual test
cases have a URI too, it will also be appended. It is possible to nest test-given
blocks, and each appends its URI segments, establishing a new current effective URI.
It is also possible to pass named parameters to test-given
, and these will be used as request parameters, passed along to Cro::HTTP::Client. Note that any named parameters that are specified to get
or request
will override those specified in test-given
.
test-given '/add', headers => { X-Precision => '15' } {
...
}
The second form doesn't require a relative URI, and instead just takes options:
test-given headers => { X-Precision => '15' } {
...
}