Bastion is a Java-based library for testing HTTP APIs and endpoints. Developers can use Bastion to test any type of HTTP service but the library also provides built-in classes for testing endpoints validating responses containing various different content types including JSON and Form URL encoded data.
This reference manual explains all the features of the Bastion library and how to use them. For a more detailed description of the Bastion API, please see the Bastion JavaDocs.
1. Dependency
Using Bastion for your project is easily done by using a dependency management tool. Bastion is available on the Maven Central repository.
1.1. Maven
For Java projects with a pom.xml
file, you can add the following dependency to your <dependencies>
section to download Bastion and make
it available for your project’s tests.
<dependency> <groupId>rocks.bastion</groupId> <artifactId>bastion</artifactId> <version>0.7-BETA</version> <scope>test</scope> </dependency>
1.2. Groovy Grapes
If you are using Groovy scripts for your tests, you can easily download Bastion directly from within your script files, using Groovy Grapes.
Add the following line to your import
statement to download Bastion and make it available to your script.
@Grapes( @Grab(group='rocks.bastion', module='bastion', version='0.7-BETA') ) import rocks.bastion.Bastion
1.3. Gradle
If you’re using Gradle as your dependency management tool, add the following line to your Gradle file to use Bastion.
testCompile group: 'rocks.bastion', name: 'bastion', version: '0.7-BETA'
2. Quickstart
Bastion makes it very easy to write simple API tests. Let us look at a test for an API which returns JSON.
@Test
public void quickstart() {
Bastion.request("Get the Restaurant's Name", GeneralRequest.get("http://localhost:9876/restaurant"))
.withAssertions((statusCode, response, model) -> assertThat(model).isEqualTo("The Sushi Parlour"))
.call();
}
3. Overview
Bastion tests are implemented using the Bastion
builder. This class provides a fluent-like interface for specifying tests. In the code snippet
below, we show how all the methods of the builder would look like when executed together.
Bastion.request(...) (1) .bind(...) (2) .withAssertions(...) (3) .call() (4) .getModel() (5)
This shows the basic structure of a Bastion test. Each one of the methods listed above (except request() and call() ) is optional but they still
must be specified in the order above. If you chose to skip withAssertions() , for example, you must call bind() before call() . The list below
explains each one of these methods separately.
|
1 | request() : This method is the main entry-point to create a Bastion test. You must specify a name which will identify this test in test reports
and also provide a request object that tells Bastion what kind of HTTP request to send. Bastion provides a number of different
built-in requests you can use (eg. JsonRequest ) but you can also implement your own request types. For a list of all built-in requests see
[Requests](#requests). The [Custom Requests](#custom-requests) section explains how to implement your own requests. |
2 | bind() : Tells Bastion which model class to use when interpreting the incoming HTTP entity. When Bastion receives a response from the remote
server, it will decode the received entity data into an object of the given type. If this decoding process fails for some reason,
the entire test is marked as failed. By providing a model class using the bind() method, you’ll have this type information available
for later on when calling the withAssertions() , thenDo() , getModel() and getResponse().getModel() methods. |
3 | withAssertions() : Takes an assertions object which will verify that the response returned by the remote server is correct.
Bastion provides a number of different built-in assertion objects for common verifications you might want to do (eg. JsonResponseAssertions ) but you can also
implement your own assertions. For a list of all built-in assertions see [Assertions](#assertions). The
[Custom Assertions](#custom-assertions) section explains how to implement your own assertions. |
4 | call() : Executes the API request configured with the previous commands. Any assertions will be applied on the received response. The call operation
will fail if Bastion is unable to bind the received response to a model or the assertions fail. |
5 | getResponse() : After the call() method is executed, you can get the HTTP response object received using the getResponse() method. The returned
response object will contain the bound model obtained from the response data. |
6 | getModel() : After the call() method is executed, you can get the bound model obtained from the response data. |
7 | getView() : After the call() method is executed, you can get a specified Java object which represents the response data. |
4. Requests
Request objects are passed to the request()
method which is the first builder method invoked when using the Bastion
builder. A Request
object
defines the HTTP data that is sent to the remote server while the test is executing. We suggest using one of the built-in Request
subclasses when
supplying your request data. Alternatively, if none of the built-in request subclasses are useful, you can create your own Request
subclass
as explained in the section Custom Requests
Bastion provides the following list of built-in Request
subclasses:
-
GeneralRequest: A simple HTTP request which allows for any arbitrary entity data.
-
JsonRequest: An HTTP request which takes a JSON string as its entity data.
-
FormUrlEncodedRequest: An HTTP request which takes data in the form of a map which is then sent as a URL encoded string as if the data was submitted using an HTML form.
Bastion.request( GeneralRequest.get("http://sushi-shop.test/sushi/{id}") .addRouteParam("id", "5") .addQueryParam("amount", "6") .addHeader("X-Caches", "disabled") ).call();
Any Request
supports the following attributes, some of which are standard to HTTP:
-
Headers: Use the
addHeader()
method to add a header to a request. -
Query Parameters: Use the
addQueryParam()
method to add a query parameter to a request. -
Route Parameters: Use the
addRouteParam()
method to add a route parameter value to a request. Route parameters are placeholder variables (delimited using a pair of braces) in the request’s URL which are then replaced by values which you specify using theaddRouteParam()
method. The following is an example of a URL with route parameters:
http://reddit.com/r/{subreddit}
-
Content Type: The content type header describes the format for the data in the request’s payload. The content type is expressed as a MIME type string. The content type may change how Bastion formats the request’s entity type body. See the particular request’s section in this user guide for more information about how to set the content type.
-
Timeout: Set a maximum timeout, in milliseconds, for the request. If the response does not arrive within the specified number of seconds the test fails its assertions. Use the
setTimeout()
method to change the timeout. -
Entity Body: Contains the payload data that is sent with the request. Each different type of request defines its own way of accepting a body object.
JsonRequest
for example accepts a file, JSON string or template and the accepted data will be sent as JSON in the request’s body. You need to see the specific request’s type documentation for more information about how to provide the entity body data.
4.1. General Request
GeneralRequest
is the universal HTTP request, able to take any arbitrary entity data string. To initialise a new GeneralRequest
use any of the following
static factory methods, giving the URL you want to send the request on:
-
GeneralRequest.get()
: Initialise an HTTPGET
request. -
GeneralRequest.post()
: Initialise an HTTPpost()
request. This method also takes a string to use as the HTTP entity data (useGeneralRequest.EMPTY_BODY
to send no data). -
GeneralRequest.delete()
: Initialise an HTTPdelete()
request. This method also takes a string to use as the HTTP entity data (useGeneralRequest.EMPTY_BODY
to send no data). -
GeneralRequest.put()
: Initialise an HTTPput()
request. This method also takes a string to use as the HTTP entity data (useGeneralRequest.EMPTY_BODY
to send no data). -
GeneralRequest.patch()
: Initialise an HTTPpatch()
request. This method also takes a string to use as the HTTP entity data (useGeneralRequest.EMPTY_BODY
to send no data).
Calling any of the above methods will give you an initialised GeneralRequest
object which can be used with Bastion.request()
. The request will not initially
have any HTTP headers, query parameters or route parameters.
Once you have an instance of GeneralRequest
, you can call methods to modify Headers, Query Parameters, Route Parameters as
explained in section [request-attributes].
By default, the GeneralRequest will have content type text/plain . Use setContentType() on the request to change the content type
to something else.
|
Bastion.request(
GeneralRequest.get("http://sushi-shop.test/sushi")
).call();
Bastion.request(
GeneralRequest.get("http://sushi-shop.test/sushi/{id}")
.addRouteParam("id", "5")
.addQueryParam("amount", "6")
.addHeader("X-Caches", "disabled")
).call();
Bastion.request(GeneralRequest.post("http://sushi-shop.test/greeting", "<b>Hello, sushi lover!</b>")
.setContentType(ContentType.TEXT_HTML)
).call();
4.2. File Request
If you want to load entity data from a file instead of typing it out in source code, use FileRequest
. This is very similar to GeneralRequest
,
in that you are not restricted to the type of data you’re sending. To initialise a new FileRequest
use any of the following
static factory methods, giving the URL you want to send the request on and a resource URL. Bastion will load the data from the specified URL
and send it as the request data.
-
FileRequest.post()
. -
FileRequest.delete()
. -
FileRequest.put()
. -
FileRequest.patch()
. -
FileRequest.withMethod()
: This factory method also accepts an HTTP method of your choice.
Bastion will attempt to guess what should go into the Content-type
header depending on the filename. If a MIME type could not be chosen,
application/octet-stream
will be used as the MIME type.
Bastion.request(FileRequest.post("http://sushi-shop.test/greeting", "classpath:fixture/greeting.html")
.setContentType(ContentType.TEXT_HTML)
).call();
4.3. JSON Request
JsonRequest
is a request object specially designed to handle JSON data. Unlike GeneralRequest
, JsonRequest
will set the appropriate content type header
to indicate that the data being sent has mime-type application/json
. The request object is initialised using a JSON string (or file) and will validate the
given data to ensure that it is valid JSON (if you don’t want this validation, use GeneralRequest
instead). To initialise a new JsonRequest
use any of the
following static factory methods, giving the URL you want to send the request on:
-
JsonRequest.fromString()
: Allows you to create aJsonRequest
with the given HTTP method (GET
,POST
, etc.) and the given JSON string. -
JsonRequest.fromResource()
: Allows you to create aJsonRequest
with the given HTTP method. The JSON data to send is loaded from the given file or classpath resource. -
JsonRequest.fromTemplate()
: LikefromResource()
but this method will also take a map of template variable names to replacement values as keys and a Mustache template file. The template data is loaded and the variables replaced by the values in the given map. The resulting data is then used as the JSON entity for the request. -
JsonRequest.fromModel()
: Allows you to create aJsonRequest
with the given HTTP method. The provided object is serialized, using the Jackson library, into a JSON string.
The request object here will validate that the provided data is valid JSON, in all cases. If you want to send invalid JSON, see General Request instead. |
The factory methods above also have utility methods which do not take an HttpMethod
argument as follows:
-
JsonRequest.postFromString()
-
JsonRequest.postFromResource()
-
JsonRequest.postfromTemplate()
-
JsonRequest.putFromString()
-
JsonRequest.putFromResource()
-
… and so on.
Bastion.request(
JsonRequest.postFromString("http://sushi-shop.test/sushi", "{ \"name\": \"Salmon Nigiri\", \"price\":5.85 }")
).call();
Bastion.request(
JsonRequest.patchFromString("http://sushi-shop.test/sushi/2",
"{ \"op\":\"replace\", \"path\":\"/name\", \"value\":\"Squid Nigiri\" }")
).call();
Use the fromString()
family of static factory methods to directly supply the JSON data to use in the test. You can simply type in
your JSON request, as you would using an HTTP client, and Bastion will take care of all the other details related to JSON requests
for you.
In a language like Java, typing the request data directly in the test can quickly start becoming unwieldy due to all the extra escape characters you need. We recommend using a language like Groovy, which supports multi-line strings, allowing you to avoid all the unnecessary escape characters. |
Bastion.request(
JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json")
).call();
Use the fromResource()
family of static factory methods to load a simple JSON file as the HTTP entity body. The fromResource()
methods can take any URL including those beginning with the classpath:
prefix (which loads a file from the
classpath).
Bastion.request(
JsonRequest.postFromTemplate("http://sushi-shop.test/sushi", "classpath:/rocks/bastion/core/request/test-template-body.json",
Collections.singletonMap("food", "Squid Nigiri"))
).call();
The fromTemplate()
family of static factory methods are similar to fromResource()
but they also take any additional argument
containing a map where the keys are variable names and the values are the replacement values for the variable placeholders in the
template.
The template files must be Mustache templates. An example Mustache template is shown below:
{
"name": "john",
"timestamp": "2016-10-15T20:00:25+0100",
"favourites": {
"food": "{{ food }}",
"colours": ["blue", "red"],
"number": 23
}
}
Notice the food
variable in the template: this will get replaced by the value apples
in the test above.
Sushi model = Sushi.newSushi().id(19).name("Salmon Nigiri").price(10L).type(Sushi.Type.NIGIRI).build();
Bastion.request(JsonRequest.postFromModel("http://test.test", model)).call();
Use the fromModel()
family of static factory methods to send any Java object serialized as a JSON string. You can annotate your object’s
class with Jackson annotations to customise how the provided object is serialized.
Bastion.request(
JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json")
.overrideContentType(ContentType.APPLICATION_OCTET_STREAM)
).call();
Once you have an instance of JsonRequest
, you can call methods to modify Headers, Query Parameters and Route Parameters as
explained in section Request Attributes. You can also change the Content type header that is sent using overrideContentType()
(by default, application/json
is sent).
4.4. Form URL Encoded Data Request
FormUrlEncodedRequest
is a request object that allows you send URL encoded data as part of the HTTP request. This request is equivalent to
requests sent by HTML forms (hence the Form
in the name). The request will automatically be configured to have the mime-type application/x-www-form-urlencoded
.
Unlike JsonRequest
, after initialising a ForumUrlEncodedRequest
, you will need to call additional methods to fill in the request’s data.
First, use any of the following static factory methods and specify the URL to send the request to:
-
FormUrlEncodedRequest.post()
-
FormUrlEncodedRequest.put()
-
FormUrlEncodedRequest.delete()
-
FormUrlEncodedRequest.patch()
-
FormUrlEncodedRequest.withMethod()
The withMethod() factory method allows you choose any HTTP method you want (including GET ). Use it when none of the
other standard factory methods are suitable for your test.
|
Use the addDataParameter()
or addDataParameters()
methods to add the data
which will go into the request’s entity body. The FormUrlEncodedRequest
will automatically format the data you supply, internally,
into a URL encoded string. An example, using FormUrlEncodedRequest
follows below,
Bastion.request(
FormUrlEncodedRequest.post("http://sushi-shop.test/sushi")
.addDataParameter("name", "Squid Nigiri")
.addDataParameter("price", "5.85")
).call();
Bastion.request(
FormUrlEncodedRequest.put("http://sushi-shop.test/booking")
.addDataParameter("name", "John Doe")
.addDataParameter("timestamp", "2017-02-10T19:00:00Z")
.addHeader("X-Manager", "Alice")
.overrideContentType(ContentType.APPLICATION_OCTET_STREAM)
).call();
Once you have an instance of FormUrlEncodedRequest
, you can call methods to modify Headers, Query Parameters and Route Parameters as
explained in section Request Attributes. You can also change the Content type header that is sent using
overrideContentType()
(by default, application/x-www-form-urlencoded
is sent).
4.5. Custom Requests
Bastion gives you the option of developing your own request classes. This is useful if you notice that you are repeatedly using a particular request in your tests. For the sake of maintainability and better software design, you can avoid repeatedly initialising the same request, over and over again, by implementing your own request type.
The relevant interface to implement is HttpRequest
. This interface defines the following methods which you need to implement:
-
name()
: Returns a descriptive name of the current request object. This name might appear in test reports, so returning a good name helps you debug faster when a problem occurs. -
url()
: Returns the URL string which the request will be sent to. Bastion is quite lenient on what constitutes a valid URL. If a question mark appears in the URL, for example, anything after the question mark will be added to the request’s query parameters. -
method()
: Returns the HTTP method that the request will be sent with. This could beGET
,POST
,PUT
, etc. -
contentType()
: Returns the value which will be used for theContent-type
HTTP header. You are not required to return a content type, hence this method returns anOptional
value. -
headers()
: Returns the possibly emptyCollection
ofApiHeader
objects (or rather, HTTP headers) which will be sent with the request. -
queryParams()
: Returns the possibly emptyCollection
ofApiQueryParam
objects which are sent with the HTTP request. -
routeParams()
: Returns the values to use for filling in any route parameters in this request’s URL. Route parameters are variables, enclosed within a pair of braces, in the requet’s URL. -
timeout()
: Returns a number, in milliseconds, after which the request will timeout and the request fails. ReturnHttpRequest.USE_GLOBAL_TIMEOUT
to use the configured default timeout. You do not need to implement this method if you want to use the default value. -
body()
: Returns the object to use for the HTTP entity body. Bastion will typically send the returned object’stoString()
value but this might be depend on the request’s content type.
Once you’ve implemented your own HttpRequest
, you can then pass it, as if it was any other request, to the Bastion.request()
method.
The built-in HttpRequest
implementations provided with Bastion are good examples. A simpler example is found in the test sources called
CreateSushiRequest
.
If you’ve developed a HttpRequest implementation which you think might be useful for the Bastion community and other users,
please consider submitting a pull request to the main Bastion repository. See the Contribute section for more information.
|
5. Assertions
Assertions objects are passed to the withAssertions()
method which is called either after the request()
method or the bind()
method when using the
Bastion
builder. An Assertions
objects defines the test predicate applied on the received HTTP response. If any of the applied assertions fail, then
the test fails. Certain Assertions
objects will provide helpful messages and logs to explain how to transform the received response into the expected response.
When supplying Assertions
using the withAssertions()
method, you can use the and()
method on the Assertions themselves to chain Assertions
together.
We suggest using one of the built-in Assertions
subclasses when defining your tests. Alternatively, if none of the built-in assertions subclasses are
useful, you can create your own Assertions
subclass as explained in the section Custom Assertions.
Bastion provides the following list of built-in Assertions
subclasses.
-
JsonResponseAssertions: Asserts that a received response is in JSON format and that the received response data is as expected.
-
JsonSchemaAssertions: Asserts that a received response is in JSON format and that the received response data at least conforms to the given JSON schema.
-
StatusCodeAssertions: Asserts that a received response has any of the expected HTTP status codes.
5.1. JSON Assertion
JsonResponseAssertions
lets you test that specific JSON data has been received. The expected JSON data
can be given as a JSON string, loading from a file or loaded and compiled from a template file.
It is important to realise that when comparing the expected JSON with the actual JSON received in the response, Bastion will be smart enough to ignore any trivial differences in the data. In particular, JSON is compared structurally as opposed to a straight-up string equality check. Properties (not array values!) may be in a different order and there might be whitespace in the received data, but unless the JSON structure is different, the test will pass.
First, you must initialise a JsonResponseAssertions
object using one of the three static factory methods:
-
JsonResponseAssertions.fromString()
: Allows you to create aJsonResponseAssertions
expecting the given HTTP status code and the given JSON. -
JsonResponseAssertions.fromResource()
: Allows you to create aJsonResponseAssertions
expecting the given HTTP status code. The JSON data to assert for is loaded from the given file or classpath resource. -
JsonResponseAssertions.fromTemplate()
: LikefromResource()
but this method will also take a map of template variable names to replacement values as keys and a Mustache template file. The template data is loaded and the variables replaced by the values in the given map. The resulting data is then used as the expected JSON entity for the assertions.
The assertions object here will validate that the provided expected JSON is valid. |
Bastion.request(GeneralRequest.get("http://sushi-shop.test/reservation/1"))
.withAssertions(JsonResponseAssertions.fromString(200, "{ \"name\":\"John Doe\", \"timestamp\":\"2016-02-10T21:00:00Z\" }"))
.call();
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.withAssertions(JsonResponseAssertions.fromResource(201, "classpath:/json/create_sushi_response.json")
.ignoreValuesForProperties("id")
).call();
JsonResponseAssertions
give some very useful insight into what your API under test is doing wrong. For example, the following test has been
set up to fail.
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.withAssertions(JsonResponseAssertions.fromString(201, "{ " +
"\"id\":5, " +
"\"name\":\"sashimi\", " +
"\"price\":\"EUR 5.60\", " +
"\"type\":\"SASHIMI\" " +
"}"
).ignoreValuesForProperties("id")).call();
Since the expected and actual JSON is compared structurally, Bastion outputs a JSON patch which describes exactly how to transform the actual JSON into the received JSON. For the test above, for example, we get:
java.lang.AssertionError: Actual response body is not as expected.
The following JSON Patch (as per RFC-6902) tells you what operations you need to perform to transform the actual response body into the expected response body:
[{"op":"replace","path":"/price","value":"EUR 5.60"}]
For the above test to pass, we need to replace the value of the price
property in the API with the value EUR 5.60
.
Once you have a JsonResponseAssertions
object, you can call the following methods on it, which will change the behaviour of the assertions
Bastion performs:
-
overrideContentType()
: Changes the expected content type header of the response. By default, Bastion will check that the content type isapplication/json
for the assertions to pass. You can change it using this method, if you need to. -
ignoreValuesForProperties()
: Ignores the value returned in the response for the specified JSON properties. This is useful if you have an auto-generated ID in the response, for example. Bastion will still check that the property appears in the response but will ignore any difference in its value.
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.withAssertions(JsonResponseAssertions.fromString(201, "{ " +
"\"id\":5, " +
"\"name\":\"sashimi\", " +
"\"price\":5.60, " +
"\"type\":\"SASHIMI\" " +
"}"
).ignoreValuesForProperties("id")
).call();
5.2. JSON Schema Assertion
This assertions object is similar to JsonResponseAssertions but takes a JSON schema as its input. The assertions object will check that the received response conforms to the given JSON schema. This is useful when you do not care about the actual content of the response (or perhaps you don’t know what it will be yet) but do care that it is always in a consistent format.
First, you must initialise a JsonSchemaAssertions
object using one of the two static factory methods:
-
JsonSchemaAssertions.fromString()
: Allows you to create aJsonSchemaAssertions
where the schema to test for is supplied as a string argument to this method. -
JsonSchemaAssertions.fromResource()
: Allows you to create aJsonSchemaAssertions
where the schema to test for is loaded from the given file or classpath resource.
Just like JsonResponseAssertions , this assertions object supports calling overrideContentType() to change
the expected content-type header.
|
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.withAssertions(JsonSchemaAssertions.fromResource("classpath:/json/create_sushi_response_schema.json")).call();
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.withAssertions(JsonSchemaAssertions.fromString("{" +
" \"$schema\": \"http://json-schema.org/draft-04/schema#\"," +
" \"type\": \"object\"," +
" \"properties\": {" +
" \"id\": {\n" +
" \"type\": \"integer\"" +
" }" +
" }," +
" \"required\": [" +
" \"id\"" +
" ]" +
"}")).call();
As you can see from the example above, JSON Schema quickly becomes very unwieldy to type out in a Java source file. We strongly recommend supplying the schema from an external resource or, if you really must type out the schema as a string, use an alternative JVM language which supports multiline strings (like Groovy). |
5.3. Status Code Assertions
This is a simple assertions object which asserts that the HTTP status code of the response is as expected. You can supply multiple expected status codes and the assertion will pass if the status code matches any of the given expected status codes.
To instantiate a StatusCodeAssertions
object use the StatusCodeAssertions.expecting()
static factory method. The static factory method
takes any number of HTTP status codes which will all be considered as valid response codes.
Bastion.request(GeneralRequest.post("http://sushi-shop.test/greeting", "<b>Hello, sushi lover!</b>"))
.withAssertions(StatusCodeAssertions.expecting(200)).call();
Bastion.request(GeneralRequest.post("http://sushi-shop.test/greeting", "<b>Hello, sushi lover!</b>"))
.withAssertions(StatusCodeAssertions.expecting(200, 201, 204)).call();
5.4. Custom Assertions
Bastion gives you the option of developing your own assertions classes. This is useful if you notice that you are repeatedly using a particular assertion in your tests. For the sake of maintainability and better software design, you can avoid repeatedly initialising the same assertions, over and over again, by implementing your own assertions type.
The relevant interface to implement is Assertions
. This interface defines a single method which you need to implement: execute()
.
The execute()
method takes the following parameters, which Bastion will provider when the test runs:
-
statusCode
: The HTTP status code of the response. -
response
(type:ModelResponse
): The response object which represents the received response. This object will also contain the object that Bastion has bound from the response. You can also obtain an alternate view of the response (which is not necessarily the model) usinggetView()
. -
model
: The object that Bastion has bound from the response. This is provided for convenience so that you don’t need to useresponse.getModel()
every time.
Notice that Assertions
takes a generic type parameter. This type parameter describes the type of response model that the Assertions
object is
expecting. By supplying a good generic type argument in your Assertions
subclass, the user will always have to bind()
that model type before
using your Assertions
object (see Binding a Model section).
Once you’ve implemented your own Assertions
, you can then pass it, as if it was any other assertions object, to the withAssertions()
method.
The built-in Assertions
implementations provided with Bastion are good examples.
If you’ve developed an Assertions implementation which you think might be useful for the Bastion community and other users,
please consider submitting a pull request to the main Bastion repository. See the Contribute section for more information.
|
5.5. Lambda Assertions
Since the Assertions
interface explained in Custom Assertions contains only one method, it is a functional interface
which you can implement using a lambda as shown in the next example (the example below contains AssertJ assertions):
Bastion.request(GeneralRequest.post("http://sushi-shop.test/greeting", "<b>Hello, sushi lover!</b>"))
.withAssertions((statusCode, response, model) -> {
assertThat(statusCode).describedAs("Response Status Code").isEqualTo(200);
assertThat(response.getHeaders()).describedAs("Response Headers").contains(new ApiHeader("Author", "John Doe"));
}).call();
6. Binding a Model
Most of the time, you will want to extract the data received in an HTTP response, during a test, as a Java object. This lets you more easily perform assertions on the Java object itself or perhaps even use the information contained within for a future request inside the same test.
The process for extracting a Java object from the HTTP response data is called Binding a Model and is achieved by using the bind()
method
after calling Bastion.request()
. bind()
takes a Java class type which you’d like to extract.
For example, if the received data is a JSON-formatted string, you can extract a Java object representation of the JSON data by supplying the
Java type which corresponds to whatever JSON you’re expecting. You can also get the JSON AST object bound by Jackson, by binding to JsonNode
directly.
Once you have bound a model type using bind()
, the typed model object is available to whatever Assertions
object you passed to the
withAssertions()
method.This is particularly useful if you supply a lambda expression to the withAssertions()
method as the model object
will be correctly typed and all the data will be directly available in Java.
When receiving application/json
data, if you supply any Java object type to the bind()
method, Bastion will use the Jackson library to
deserialise the received JSON data to whatever object type you supplied. This means that you can modify the way data is deserialised in your
data class using Jackson Annotations.
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.bind(Sushi.class)
.withAssertions((statusCode, response, model) -> {
assertThat(statusCode).isEqualTo(201);
assertThat(response.getContentType()).hasValueSatisfying(contentType ->
assertThat(contentType.getMimeType()).isEqualToIgnoringCase("application/json")
);
assertThat(model.getName()).isEqualTo("sashimi");
}).call();
7. Executing and Getting Data
Once you have configured a request and an assertions object for Bastion to execute, you can start the test using the call()
method. Bastion
will execute the request supplied in request()
, bind the response to a applicable views (including a model of the type specified in bind()
)
and finally assert that the assertions given in withAssertions()
hold true.
Once executed, you can retrieve the response data as follows:
-
getResponse()
: Returns a complete HTTPResponse
object containing all HTTP headers and the content body. It will also contain the model object which was bound by Bastion. -
getModel()
: Returns the model object which was bound by Bastion. This is a shortcut method forgetResponse().getModel()
. -
getView(…)
: Returns an alternative view object which was bound by Bastion. A view is a Java object which represents the response data which was decoded by Bastion. The model returned bygetModel()
is in fact one of the views of the response.
ModelResponse<? extends Sushi> response =
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.bind(Sushi.class)
.call()
.getResponse();
System.out.println(response.getHeaders());
Sushi sushi =
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.bind(Sushi.class)
.call()
.getModel();
System.out.println(sushi.getName());
Optional<JsonNode> json =
Bastion.request(JsonRequest.postFromResource("http://sushi-shop.test/sushi", "classpath:/json/create_sushi_request.json"))
.bind(Sushi.class)
.call()
.getView(JsonNode.class);
8. Global Configuration
Global configuration allows you to add request attributes to all requests that are performed by Bastion. This is extremely useful, for example,
if you have to add an authorization header to your requests after logging in. You can access the following request attributes through Bastion.globals
:
-
Global Headers: Use
addHeader()
orremoveHeader()
to modify the global HTTP headers which are appended to every HTTP request. -
Global Query Parameters: Use
addQueryParam()
orremoveQueryParam()
to modify the global query parameters which are appended to every HTTP request. -
Global Route Parameters: Use
addRouteParam()
orremoveRouteParam()
to modify the global route parameters which are appended to every HTTP request. -
Global Timeout: Use
setTimeout()
to set the timeout value for all HTTP requests (unless otherwise specified differently for a particular request).
Bastion.globals()
.addHeader("Authorization", "BASIC a3lsZTpwdWxsaWNpbm8=")
.addQueryParam("diet", "vegetarian")
.addRouteParam("version", "v2");
Bastion.globals().clear();
To reset all globals back to their original state, use Bastion.globals().clear()
.
Bastion.globals().clear();
In tests which set request globals, you will want to clear them afterwards using JUnit’s @After annotation unless you want them
to persist across multiple tests.
|
9. Contribute
Bastion is an open-source project! Open-source means that we encourage you to contribute in any way you can. We will accept all contributions, in any shape or form, that help make Bastion better. Here are some things you can do to contribute:
-
Send a positive comment to the Bastion contributers. :)
-
Submit an issue on GitHub containing a bug report or suggestion. We ask you to spend a couple minutes before submitting an issue to check that it has not been submitted earlier. When opening an issue, try to include as much detail as possible so that the community can more easily address your concern.
-
Submit a pull request for any of our open issues. Some issues are more easy to implement than others and, if you’re just starting out, these issues let you get used to the Bastion code structure. If you need any assistance, simply comment on the issue at hand and we’ll be glad to help. We ask that you adhere to a consistent code style and employ good programming practice but don’t worry if you’re unsure about anything: we’ll help you get your submission up to scratch as well.
-
You can also submit a pull request which is not related to any of the issues currently on GitHub. If you have developed your own
Request
orAssertions
implementations, for example, and you believe they could be useful to the rest of the Bastion community, we will add them to the library for use in future versions of Bastion. -
Make our User Guide better. Our User Guide is very important to us and we strive to keep it as up to date as possible. If you spot any omissions, typos, grammatical errors or have an idea of how it can be improved, please submit a pull request. The files for our user guide can be found in the
src/docs/asciidoc
directory. -
Spread the word. Tell your colleagues about Bastion or write a blog post about Bastion. The more people we can tell Bastion about, the better!