Class Jadler
- java.lang.Object
-
- net.jadler.Jadler
-
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.
Jadler Usage Basics
Let's have a simple component with one operation:
public interface AccountManager { Account getAccount(String id); }An implementation of the
getAccountoperation is supposed to send a GET http request to/accounts/{id}where{id}stands for the methodidparameter, deserialize the http response to anAccountinstance and return it. If there is no such account (the GET request returned 404),nullmust 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_JSONconstant) 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
localhostusing the http protocol (https will be supported in a latter version of Jadler) connecting to a port which can be retrieved using theport()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 returnsnullif 404 is returned from the server. The second one tests a runtime exception is thrown upon 500 http response.Multiple responses definition
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).
More suitable stub rules
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
/accountsboth rules would be applicable. However, the latter stub gets priority over the former one. In this example, an http response with202status code would be returned.Advanced http stubbing
The WHEN part
So far two
having*methods have been introduced,RequestMatching.havingMethodEqualTo(java.lang.String)to check the http method equality andRequestMatching.havingPathEqualTo(java.lang.String)to check the path equality. But there's more!You can use
RequestMatching.havingBodyEqualTo(java.lang.String)andRequestMatching.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)orRequestMatching.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 useRequestMatching.havingParameter(java.lang.String),RequestMatching.havingParameters(java.lang.String[])orRequestMatching.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
POSTrequest to/accountswith the specified body,application/jsoncontent type header and aforcehttp parameter set to1.The THEN part
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)andResponseStubbing.withBody(java.io.Reader)methods are for. These are very often used in conjunction ofResponseStubbing.withEncoding(java.nio.charset.Charset)to define the encoding of the response bodyIf you'd like to define the stub response body binary, feel free to use either
ResponseStubbing.withBody(byte[])orResponseStubbing.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 theContent-Typeheader you can use specially tailoredResponseStubbing.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-TypeandLocationheaders set to proper values.Even more advanced http stubbing
Fine-tuning the WHEN part using predicates
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)andRequestMatching.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 theRequestMatching.that(org.hamcrest.Matcher)method://meetsCriteria() is some factory method returning a Matcher<Request> instance onRequest() .that(meetsCriteria()) .respond() .withStatus(204);Fine-tuning the THEN part using defaults
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-Typeheader set totext/plain, response body encoded usingISO-8859-1and a header namedX-DEFAULT-HEADERset todefault_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-8will be used by default.Generating a stub response dynamically
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 theResponderinterface 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 aResponderimplementation as a parameter, Jadler subsequently uses theResponder.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
200for even requests and500for odd requests. And the value of thex-custom-request-headerrequest header is used as a response header.As you can see in the example this
Responderimplementation is thread-safe (by usingAtomicIntegerhere). 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.
Request Receipt Verification
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
AccountManagerinterface:public interface AccountManager { Account getAccount(String id); void deleteAccount(String id); }The
deleteAccountoperation is supposed to delete an account by sending aDELETEhttp request to/accounts/{id}where{id}stands for the operationidparameter. 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
deleteAccountis invoked. However in this test case we would like to test whether theDELETEhttp 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 samehaving*methods which has been already described in the stubbing section (the methods are defined in theRequestMatchinginterface).The request definition must be followed by calling one of the
received*methods. The already introducedVerifying.receivedOnce()method verifies there has been received exactly one request meeting the given criteria so far. If the verification fails aVerificationExceptioninstance is thrown and the exact reason is logged on theINFOlevel.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 finallyVerifying.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 inIllegalStateException.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.
Jadler Lifecycle
As already demonstrated, the standard Jadler lifecycle consists of the following steps:
- starting Jadler including the underlying http server (by calling one of the
initJadler*methods of theJadlerfacade) in the setUp phase of a test - stubbing using the
onRequest()method at the beginning of the test method - calling the code to be tested
- doing some verification using
verifyThatRequest()if necessary - closing Jadler including the underlying http server (by calling the
closeJadler()) method in the tearDown phase of a test
These steps are then repeated for every test in a test suite. This lifecycle is fully covered by the static
Jadlerfacade which encapsulates and manages an instance of the coreJadlerMockercomponent.Creating mocker instances manually
There are few specific scenarios when creating
JadlerMockerinstances manually (instead of using theJadlerfacade) 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(); } }Simplified Jadler Lifecycle Management
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
net.jadler.junit.rule.JadlerRulecan 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 callinginitJadler()andcloseJadler()in thesetUpandtearDownmethods.To use this rule the
jadler-junitartifact must be on the classpath.
-
-
Nested Class Summary
Nested Classes Modifier and Type Class Description static classJadler.OngoingConfigurationThis class serves as a DSL support for additional Jadler configuration.
-
Method Summary
All Methods Static Methods Concrete Methods Modifier and Type Method Description static voidcloseJadler()Stops the underlyingStubHttpServerand closes Jadler.static Jadler.OngoingConfigurationinitJadler()Initializes Jadler and starts a default stub servernet.jadler.stubbing.server.jetty.JettyStubHttpServerserving the http protocol listening on any free port.static Jadler.OngoingConfigurationinitJadlerListeningOn(int port)Initializes Jadler and starts a default stub servernet.jadler.stubbing.server.jetty.JettyStubHttpServerserving the http protocol listening on the given port.static Jadler.OngoingConfigurationinitJadlerUsing(StubHttpServer server)Initializes Jadler and starts the givenStubHttpServer.static RequestStubbingonRequest()Starts new http stubbing (defining new WHEN-THEN rule).static intport()Use this method to retrieve the port the underlying http stub server is listening onstatic voidresetJadler()Resets Jadler by clearing all previously created stubs as well as stored received requests.static VerifyingverifyThatRequest()Starts new verification (checking that an http request with given properties was or was not received)
-
-
-
Method Detail
-
initJadler
public static Jadler.OngoingConfiguration initJadler()
Initializes Jadler and starts a default stub server
net.jadler.stubbing.server.jetty.JettyStubHttpServerserving the http protocol listening on any free port. The port number can be retrieved usingport().This should be preferably called in the
setUpmethod of the test suite.- Returns:
Jadler.OngoingConfigurationinstance for additional configuration and tweaking (use itswith*methods)
-
initJadlerListeningOn
public static Jadler.OngoingConfiguration initJadlerListeningOn(int port)
Initializes Jadler and starts a default stub server
net.jadler.stubbing.server.jetty.JettyStubHttpServerserving the http protocol listening on the given port.This should be preferably called in the
setUpmethod of the test suite.- Parameters:
port- port the stub server will be listening on- Returns:
Jadler.OngoingConfigurationinstance for additional configuration and tweaking (use itswith*methods)
-
initJadlerUsing
public static Jadler.OngoingConfiguration initJadlerUsing(StubHttpServer server)
Initializes Jadler and starts the given
StubHttpServer.This should be preferably called in the
setUpmethod of the test suite- Parameters:
server- stub http server instance- Returns:
Jadler.OngoingConfigurationinstance for additional configuration and tweaking (use itswith*methods)
-
closeJadler
public static void closeJadler()
Stops the underlying
StubHttpServerand closes Jadler.This should be preferably called in the
tearDownmethod of a test suite.
-
resetJadler
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 (usingcloseJadler()) 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.
- See Also:
JadlerMocker.reset()
-
port
public static int port()
Use this method to retrieve the port the underlying http stub server is listening on- Returns:
- the port the underlying http stub server is listening on
- Throws:
IllegalStateException- if Jadler has not been initialized yet
-
onRequest
public static RequestStubbing onRequest()
Starts new http stubbing (defining new WHEN-THEN rule).- Returns:
- stubbing object for ongoing stubbing
-
verifyThatRequest
public static Verifying verifyThatRequest()
Starts new verification (checking that an http request with given properties was or was not received)- Returns:
- verifying object for ongoing verifying
-
-