public class Jadler extends Object
This class is a gateway to the whole Jadler library. Jadler is a powerful yet simple to use http mocking library for writing integration tests in the http environment. It provides a convenient way to create a stub http server which serves all http requests sent during a test execution by returning stub responses according to defined rules.
Let's have a simple component with one operation:
public interface AccountManager { Account getAccount(String id); }
An implementation of the getAccount
operation is supposed to send a GET http request to
/accounts/{id}
where {id}
stands for the method id
parameter, deserialize the http response
to an Account
instance and return it. If there is no such account (the GET request returned 404),
null
must be returned. If some problem occurs (50x http response), a runtime exception must be thrown.
For the integration testing of this component it would be great to have a way to start a stub http server which would return predefined stub responses. This is where Jadler comes to help.
Let's write such an integration test using jUnit:
... import static net.jadler.Jadler.*; ... public class AccountManagerImplTest { private static final String ID = "123"; private static final String ACCOUNT_JSON = "{\"account\":{\"id\": \"123\"}}"; @Before public void setUp() { initJadler(); } @After public void tearDown() { closeJadler(); } @Test public void getAccount() { onRequest() .havingMethodEqualTo("GET") .havingPathEqualTo("/accounts/" + ID) .respond() .withBody(ACCOUNT_JSON) .withStatus(200); final AccountManager am = new AccountManagerImpl("http", "localhost", port()); final Account account = ag.getAccount(ID); assertThat(account, is(notNullValue())); assertThat(account.getId(), is(ID)); } }
There are three main parts of this test. The setUp phase just initializes Jadler (which includes starting a stub http server), while the tearDown phase just closes all resources. Nothing interesting so far.
All the magic happens in the test method. New http stub is defined, in the THEN part
the http stub server is instructed to return a specific http response
(200 http status with a body defined in the ACCOUNT_JSON
constant) if the incoming http request
fits the given conditions defined in the WHEN part (must be a GET request to /accounts/123
).
In order to communicate with the http stub server instead of the real web service, the tested instance
must be configured to access localhost
using the http protocol (https will be supported
in a latter version of Jadler) connecting to a port which can be retrieved using the port()
method.
The rest of the test method is business as usual. The getAccount(String)
is executed and some
assertions are evaluated.
Now lets write two more test methods to test the 404 and 500 scenarios:
@Test public void getAccountNotFound() { onRequest() .havingMethodEqualTo("GET") .havingPathEqualTo("/accounts/" + ID) .respond() .withStatus(404); final AccountManager am = new AccountManagerImpl("http", "localhost", port()); Account account = am.getAccount(ID); assertThat(account, is(nullValue())); } @Test(expected=RuntimeException.class) public void getAccountError() { onRequest() .havingMethodEqualTo("GET") .havingPathEqualTo("/accounts/" + ID) .respond() .withStatus(500); final AccountManager am = new AccountManagerImpl("http", "localhost", port()); am.getAccount(ID); }
The first test method checks the getAccount(String)
method returns null
if 404 is returned
from the server. The second one tests a runtime exception is thrown upon 500 http response.
Sometimes you need to define more subsequent messages in your testing scenario. Let's test here your code can recover from an unexpected 500 response and retry the POST receiving 201 this time:
onRequest() .havingPathEqualTo("/accounts") .havingMethodEqualTo("POST") .respond() .withStatus(500) .thenRespond() .withStatus(201);
The stub server will return a stub http response with 500 response status for the first request which suits the stub rule. A stub response with 201 response status will be returned for the second request (and all subsequent requests as well).
It's not uncommon that more stub rules can be applied (the incoming request fits more than one WHEN part). Let's have the following example:
onRequest() .havingPathEqualTo("/accounts") .respond() .withStatus(201); onRequest() .havingMethodEqualTo("POST") .respond() .withStatus(202);
If a POST http request was sent to /accounts
both rules would be applicable. However, the latter stub
gets priority over the former one. In this example, an http response with 202
status code would be
returned.
So far two having*
methods have been introduced,
RequestMatching.havingMethodEqualTo(java.lang.String)
to check the http method equality and
RequestMatching.havingPathEqualTo(java.lang.String)
to check the path equality. But there's more!
You can use RequestMatching.havingBodyEqualTo(java.lang.String)
and
RequestMatching.havingRawBodyEqualTo(byte[])
} to check the request body equality
(either as a string or as an array of bytes).
Feel free to to use RequestMatching.havingQueryStringEqualTo(java.lang.String)
to test the query string value.
And finally don't hesitate to use RequestMatching.havingParameterEqualTo(java.lang.String, java.lang.String)
or RequestMatching.havingHeaderEqualTo(java.lang.String, java.lang.String)
for a check whether there is an http parameter / header in the incoming request with a given value.
If an existence check is sufficient you can use RequestMatching.havingParameter(java.lang.String)
,
RequestMatching.havingParameters(java.lang.String[])
or
RequestMatching.havingHeader(java.lang.String)
, RequestMatching.havingHeaders(java.lang.String[])
instead.
So let's write some advanced http stub here:
onRequest() .havingMethodEqualTo("POST") .havingPathEqualTo("/accounts") .havingBodyEqualTo("{\"account\":{}}") .havingHeaderEqualTo("Content-Type", "application/json") .havingParameterEqualTo("force", "1") .respond() .withStatus(201);
The 201 stub response will be returned if the incoming request was a POST
request to /accounts
with the specified body, application/json
content type header and a force
http parameter set to
1
.
There are much more options than just setting the http response status using the
ResponseStubbing.withStatus(int)
in the THEN part of an http stub.
You will probably have to define the stub response body as a string very often. That's what the
ResponseStubbing.withBody(java.lang.String)
and
ResponseStubbing.withBody(java.io.Reader)
methods are for. These
are very often used in conjunction of
ResponseStubbing.withEncoding(java.nio.charset.Charset)
to define the
encoding of the response body
If you'd like to define the stub response body binary, feel free to use either
ResponseStubbing.withBody(byte[])
or
ResponseStubbing.withBody(java.io.InputStream)
.
Setting a stub response header is another common http stubbing use case. Just call
ResponseStubbing.withHeader(java.lang.String, java.lang.String)
to
set such header. For setting the Content-Type
header you can use specially tailored
ResponseStubbing.withContentType(java.lang.String)
method.
And finally sometimes you would like to simulate a network latency. To do so just call the
ResponseStubbing.withDelay(long, java.util.concurrent.TimeUnit)
method.
The stub response will be returned after the specified amount of time or later.
Let's define the THEN part precisely:
onRequest() .havingMethodEqualTo("POST") .havingPathEqualTo("/accounts") .havingBodyEqualTo("{\"account\":{}}") .havingHeaderEqualTo("Content-Type", "application/json") .havingParameterEqualTo("force", "1") .respond() .withDelay(2, SECONDS) .withStatus(201) .withBody("{\"account\":{\"id\" : 1}}") .withEncoding(Charset.forName("UTF-8")) .withContentType("application/json; charset=UTF-8") .withHeader("Location", "/accounts/1");
If the incoming http request fulfills the WHEN part, a stub response will be returned after at least
2 seconds. The response will have 201 status code, defined json body encoded using UTF-8 and both
Content-Type
and Location
headers set to proper values.
So far we have been using the equality check to define the WHEN part. However it's quite useful to be able to use other predicates (non empty string, contains string, ...) then just the request value equality.
Jadler uses Hamcrest as a predicates library. Not only it provides many already implemented predicates (called matchers) but also a simple way to implement your own ones if necessary. More on Hamcrest usage to be found in this tutorial.
So let's write the following stub: if an incoming request has a non-empty body and the request method is not PUT and the path value starts with /accounts then return an empty response with the 200 http status:
onRequest() .havingBody(not(isEmptyOrNullString())) .havingPath(startsWith("/accounts")) .havingMethod(not(equalToIgnoringCase("PUT"))) .respond() .withStatus(200);
You can use following having* methods for defining the WHEN part using a Hamcrest string matcher:
RequestMatching.havingBody(org.hamcrest.Matcher)
RequestMatching.havingMethod(org.hamcrest.Matcher)
RequestMatching.havingQueryString(org.hamcrest.Matcher)
RequestMatching.havingPath(org.hamcrest.Matcher)
For adding predicates about request parameters and headers use
RequestMatching.havingHeader(java.lang.String, org.hamcrest.Matcher)
and
RequestMatching.havingParameter(java.lang.String, org.hamcrest.Matcher)
methods. Since a request header or
parameter can have more than one value, these methods accept a list of strings predicates.
All introduced methods allow user to add a predicate about a part of an http request (body, method, ...).
If you need to add a predicate about the whole request object (of type Request
),
you can use the RequestMatching.that(org.hamcrest.Matcher)
method:
//meetsCriteria() is some factory method returning a Matcher<Request> instance onRequest() .that(meetsCriteria()) .respond() .withStatus(204);
It's pretty common many THEN parts share similar settings. Let's have two or more stubs returning
an http response with 200 http status. Instead of calling ResponseStubbing.withStatus(int)
during
every stubbing Jadler can be instructed to use 200 as a default http status:
@Before public void setUp() { initJadler() .withDefaultResponseStatus(200); }
This particular test setup configures Jadler to return http stub responses with 200 http
status by default. This default can always be overwritten by calling the ResponseStubbing.withStatus(int)
method in the particular stubbing.
The following example demonstrates all response defaults options:
@Before public void setUp() { initJadler() .withDefaultResponseStatus(202) .withDefaultResponseContentType("text/plain") .withDefaultResponseEncoding(Charset.forName("ISO-8859-1")) .withDefaultResponseHeader("X-DEFAULT-HEADER", "default_value"); }
If not redefined in the particular stubbing, every stub response will have 202 http status, Content-Type
header set to text/plain
, response body encoded using ISO-8859-1
and a header named
X-DEFAULT-HEADER
set to default_value
.
And finally if no default nor stubbing-specific status code is defined 200 will be used. And if no default
nor stubbing-specific response body encoding is defined, UTF-8
will be used by default.
In some integration testing scenarios it's necessary to generate a stub http response dynamically. This
is a case where the with*
methods aren't sufficient. However Jadler comes to help here with with
the Responder
interface which allows to define the stub response dynamically
according to the received request:
onRequest() .havingMethodEqualTo("POST") .havingPathEqualTo("/accounts") .respondUsing(new Responder() { private final AtomicInteger cnt = new AtomicInteger(1); @Override public StubResponse nextResponse(final Request request) { final int current = cnt.getAndIncrement(); final String headerValue = request.getHeaders().getValue("x-custom-request-header"); return StubResponse.builder() .status(current % 2 == 0 ? 200 : 500) .header("x-custom-response-header", headerValue) .build(); } });
The intention to define the stub response dynamically is expressed by using
RequestStubbing.respondUsing(net.jadler.stubbing.Responder)
. This method takes
a Responder
implementation as a parameter, Jadler subsequently uses the
Responder.nextResponse(net.jadler.Request)
method to generate stub responses for
all requests fitting the given WHEN part.
In the previous example the http status of a stub response is 200
for even requests and 500
for odd requests. And the value of the x-custom-request-header
request header is used as
a response header.
As you can see in the example this Responder
implementation is thread-safe
(by using AtomicInteger
here). This is important for tests of parallel nature
(more than one client can send requests fitting the WHEN part in parallel). Of course if requests are sent
in a serial way (which is the most common case) there is no need for the thread-safety of the implementation.
Please note this dynamic way of defining stub responses should be used as rarely as possible as it very often signalizes a problem either with test granularity or somewhere in the tested code. However there could be very specific testing scenarios where this functionality might be handy.
While the Jadler library is invaluable in supporting your test scenarios by providing a stub http server, it has even more to offer.
Very often it's necessary not only to provide a stub http response but also to verify that a specific
http request was received during a test execution. Let's add a removal operation to the already introduced
AccountManager
interface:
public interface AccountManager { Account getAccount(String id); void deleteAccount(String id); }
The deleteAccount
operation is supposed to delete an account by sending a DELETE
http request
to /accounts/{id}
where {id}
stands for the operation id
parameter. If the response status is
204 the removal is considered successful and the execution is finished successfully. Let's write an integration
test for this scenario:
... import static net.jadler.Jadler.*; ... public class AccountManagerImplTest { private static final String ID = "123"; @Before public void setUp() { initJadler(); } @After public void tearDown() { closeJadler(); } @Test public void deleteAccount() { onRequest() .havingMethodEqualTo("DELETE") .havingPathEqualTo("/accounts/" + ID) .respond() .withStatus(204); final AccountManager am = new AccountManagerImpl("http", "localhost", port()); final Account account = am.deleteAccount(ID); verifyThatRequest() .havingMethodEqualTo("DELETE") .havingPathEqualTo("/accounts/" + ID) .receivedOnce(); } }
The first part of this test is business as usual. An http stub is created and the tested method
deleteAccount
is invoked. However in this test case we would like to test whether the DELETE
http
request was really sent during the execution of the method.
This is where Jadler comes again to help. Calling verifyThatRequest()
signalizes an intention to
verify a number of requests received so far meeting the given criteria. The criteria is defined using exactly
the same having*
methods which has been already described in the stubbing section
(the methods are defined in the RequestMatching
interface).
The request definition must be followed by calling one of the received*
methods. The already
introduced Verifying.receivedOnce()
method verifies there has been received exactly one request meeting
the given criteria so far. If the verification fails a VerificationException
instance is thrown and
the exact reason is logged on the INFO
level.
There are three more verification methods. Verifying.receivedNever()
verifies there has not been
received any request meeting the given criteria so far. Verifying.receivedTimes(int)
allows to define
the exact number of requests meeting the given criteria. And finally
Verifying.receivedTimes(org.hamcrest.Matcher)
allows to apply a Hamcrest matcher on the number of
requests meeting the given criteria. The following example shows how to verify there have been at most
3 DELETE requests sent so far:
verifyThatRequest() .havingMethodEqualTo("DELETE") .receivedTimes(lessThan(4));
This verification feature is implemented by recording all incoming http requests (including their bodies). In some very specific corner cases this implementation can cause troubles. For example imagine a long running performance test using Jadler for stubbing some remote http service. Since such a test can issue thousands or even millions of requests the memory consumption probably would affect the test results (either by a performance slowdown or even crashes). In this specific scenarios you should consider disabling the incoming requests recording:
@Before public void setUp() { initJadler() .withRequestsRecordingDisabled(); }
Once the request recording has been disabled, calling Mocker.verifyThatRequest()
will result in IllegalStateException
.
Please note you should ignore this option almost every time you use Jadler unless you are really convinced about it. Because premature optimization is the root of all evil, you know.
As already demonstrated, the standard Jadler lifecycle consists of the following steps:
initJadler*
methods of the
Jadler
facade) in the setUp phase of a testonRequest()
method at the beginning of the test methodverifyThatRequest()
if necessarycloseJadler()
) method
in the tearDown phase of a testThese steps are then repeated for every test in a test suite. This lifecycle is fully covered by the static
Jadler
facade which encapsulates and manages an instance of the core JadlerMocker
component.
There are few specific scenarios when creating JadlerMocker
instances manually (instead of using the
Jadler
facade) can be handy. Some specific integration tests may require starting more than just one mocker
on different ports (simulating requesting multiple different http servers). If this is the case,
all the mocker instances have to be created manually (since the facade encapsulates just one mocker instance).
To achieve this each mocker must be created and disposed before and after every test:
public class ManualTest { private JadlerMocker mocker; private int port; @Before public void setUp() { mocker = new JadlerMocker(new JettyStubHttpServer()); mocker.start(); port = getStubHttpServerPort(); } @After public void tearDown() { mocker.close(); } @Test public void testSomething() { mocker.onRequest().respond().withStatus(404); //call the code to be tested here mocker.verifyThatRequest().receivedOnce(); } }
In all previous examples the jUnit @Before and @After sections were used to manage
the Jadler lifecycle. If jUnit 4.11 (or newer) is on the classpath a simple Jadler
rule JadlerRule
can be used instead:
public class AccountManagerImplTest { @Rule public JadlerRule jadlerRule = new JadlerRule(); ... }
This piece of code starts Jadler on a random port at the beginning of each test and closes it at the end.
A specific port can be defined as well: new JadlerRule(12345);
. Please note this is exactly the same as
calling initJadler()
and closeJadler()
in the setUp
and tearDown
methods.
To use this rule the jadler-junit
artifact must be on the classpath.
Modifier and Type | Class and Description |
---|---|
static class |
Jadler.OngoingConfiguration
This class serves as a DSL support for additional Jadler configuration.
|
Modifier and Type | Method and Description |
---|---|
static void |
closeJadler()
Stops the underlying
StubHttpServer and closes Jadler. |
static Jadler.OngoingConfiguration |
initJadler()
Initializes Jadler and starts a default stub server
JettyStubHttpServer
serving the http protocol listening on any free port. |
static Jadler.OngoingConfiguration |
initJadlerListeningOn(int port)
Initializes Jadler and starts a default stub server
JettyStubHttpServer
serving the http protocol listening on the given port. |
static Jadler.OngoingConfiguration |
initJadlerUsing(StubHttpServer server)
Initializes Jadler and starts the given
StubHttpServer . |
static RequestStubbing |
onRequest()
Starts new http stubbing (defining new WHEN-THEN rule).
|
static int |
port()
Use this method to retrieve the port the underlying http stub server is listening on
|
static void |
resetJadler()
Resets Jadler by clearing all previously created stubs as well as stored received requests.
|
static Verifying |
verifyThatRequest()
Starts new verification (checking that an http request with given properties was or was not received)
|
public static Jadler.OngoingConfiguration initJadler()
Initializes Jadler and starts a default stub server JettyStubHttpServer
serving the http protocol listening on any free port. The port number can be retrieved using port()
.
This should be preferably called in the setUp
method of the test suite.
AdditionalConfiguration#that()
to add more configurationpublic static Jadler.OngoingConfiguration initJadlerListeningOn(int port)
Initializes Jadler and starts a default stub server JettyStubHttpServer
serving the http protocol listening on the given port.
This should be preferably called in the setUp
method of the test suite.
port
- port the stub server will be listening onAdditionalConfiguration#that()
to add more configurationpublic static Jadler.OngoingConfiguration initJadlerUsing(StubHttpServer server)
Initializes Jadler and starts the given StubHttpServer
.
This should be preferably called in the setUp
method of the test suite
server
- stub http server instanceAdditionalConfiguration#that()
to add more configurationpublic static void closeJadler()
Stops the underlying StubHttpServer
and closes Jadler.
This should be preferably called in the tearDown
method of a test suite.
public static void resetJadler()
Resets Jadler by clearing all previously created stubs as well as stored received requests.
While the standard Jadler lifecycle consists of initializing Jadler and starting the
underlying stub server (using initJadler()
) in the setUp section of a test and stopping
the server (using closeJadler()
) in the tearDown section, in some specific scenarios
it could be useful to reuse initialized Jadler in all tests instead.
Here's an example code using jUnit which demonstrates usage of this method in a test lifecycle:
public class JadlerResetIntegrationTest { @BeforeClass public static void beforeTests() { initJadler(); } @AfterClass public static void afterTests() { closeJadler(); } @After public void reset() { resetJadler(); } @Test public void test1() { mocker.onRequest().respond().withStatus(201); //do an http request here, 201 should be returned from the stub server verifyThatRequest().receivedOnce(); } @Test public void test2() { mocker.onRequest().respond().withStatus(400); //do an http request here, 400 should be returned from the stub server verifyThatRequest().receivedOnce(); } }
Please note the standard lifecycle should be always preferred since it ensures a full independence of all tests in a suite. However performance issues may appear theoretically while starting and stopping the server as a part of each test. If this is your case the alternative lifecycle might be handy.
Also note that calling this method in a test body always signalizes a poorly written test with a problem with the granularity. In this case consider writing more fine grained tests instead of using this method.
JadlerMocker.reset()
public static int port()
IllegalStateException
- if Jadler has not been initialized yetpublic static RequestStubbing onRequest()
public static Verifying verifyThatRequest()
Copyright © 2016. All Rights Reserved.