Introduction

The Ersatz Server is a HTTP client testing tool, which allows for request/response expectations to be configured in a flexible manner. The expectations will respond in a configured manner to requests and allow testing with different responses and/or error conditions without having to write a lot of boiler-plate code.

The "mock" server is not really mock at all, it is an embedded Undertow HTTP server which registers the configured expectations as routes and then responds according to the expected behavior. This approach may seem overly heavy; however, testing an HTTP client can involve a lot of internal state and interactions that the developer is generally unaware of (and should be) - trying to mock those interactions with a pure mocking framework will get out of hand very quickly, and Undertow starts up very quickly.

Ersatz provides a balance of mock-like expectation behavior with a real HTTP interface and all of the underlying interactions in place. This allows for rich unit testing, which is what you were trying to do in the first place.

Ersatz is written in Groovy 2.4.x and requires a Java 8 VM due to its use of the modern functional libraries; however, the Ersatz library is written such that it may be used with Groovy or standard Java without pain or feature-loss. With that in mind, the expectation configuration allows two main forms, a Java-style chained builder, and a Groovy DSL, both of which may be used interchangeably or together, if you are using Groovy.

Ersatz is developed with testing in mind. It does not favor any specific testing framework, but it does work well with both the JUnit and Spock frameworks.

Getting Started

The ersatz library is available via Bintray (JCenter) and the Maven Central Repository; you can add it to your project using one of the following methods:

For Gradle add the following to your build.gradle file:

testCompile 'com.stehno.ersatz:ersatz:1.4.0'

For Maven add the code below to your pom.xml file:

<dependency>
    <groupId>com.stehno.ersatz</groupId>
    <artifactId>ersatz</artifactId>
    <version>1.4.0</version>
    <scope>test</scope>
</dependency>

You could then use Ersatz in a Spock test as follows:

HelloSpec.groovy
import spock.lang.Specification
import com.stehno.ersatz.ErsatzServer

class HelloSpec extends Specification {

    def 'say hello'(){
        setup:
        ErsatzServer ersatz = new ErsatzServer()

        server.expectations {
            get('/say/hello'){
                called equalTo(1)
                query 'name','Ersatz'
                responder {
                    content 'Hello Ersatz','text/plain'
                }
            }
        }

        ersatz.start()

        when:
        String result = "${ersatz.serverUrl}/say/hello?name=Ersatz".toURL().text

        then:
        result == 'Hello Ersatz'

        and:
        ersatz.verify()

        cleanup:
        ersatz.stop()
    }
}

The configured server is expecting a single call to GET /say/hello?name=Ersatz. When that call is received, the server will respond with the text/plain content Hello Ersatz. This code also verifies that the expected request was only called once (as requested) - if it was not called or called more than once, the verification and likewise the test, would fail.

A similar test could be written in JUnit with Java 8, as follows (using the provided ErsatzServerRule helper class):

HelloTest.java
import com.stehno.ersatz.ErsatzServer;
import com.stehno.ersatz.ContentType;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Before;
import okhttp3.OkHttpClient;
import okhttp3.Request;

import static org.junit.Assert.assertEquals;

public class HelloTest {

    @Rule
    public ErsatzServerRule ersatzServer = new ErsatzServerRule(ServerConfig::enableAutoStart);

    private OkHttpClient client;

    @Before
    public void before() {
        client = new OkHttpClient.Builder().build();
    }

    @Test
    public sayHello(){
        ersatzServer.expectations(expectations -> {
            expectations.get("/say/hello").called(1).query("name","Ersatz")
                .responder().content("Hello Ersatz", ContentType.TEXT_PLAIN)
        })

        String url = ersatzServer.getHttpUrl() + "/say/hello?name=Ersatz";
        okhttp3.Request request = new okhttp3.Request.Builder().url(url)).build();

        assertEquals("Hello Ersatz", client.newCall(request).execute().body().string());
    }
}

The two testing approaches are interchangeable and equally supported.

Ersatz Server

The core component for the Ersatz Mock Server is the com.stehno.erstaz.ErsatzServer class. It is used to manage the server lifecycle as well as provide a configuration interface.

Lifecycle

The lifecycle of an Ersatz server is broken down into four main states:

  1. Configuration

  2. Testing

  3. Verification

  4. Cleanup

they are detailed in the following sections.

Configuration

The first lifecycle step is "configuration", where the server is instantiated, request expectations are configured and the server is started. An Ersatz server is created by creating an instance of ErsatzServer with optional configuration performed by providing a Closure or Consumer<ServerConfig>, both of which will be given a ServerConfig instance to perform configuration operations on.

Tip
Configuration of encoders and decoders via the global configuration mechanism are considered global and will be used as defaults across all expectation configurations.

At this point there is no HTTP server running and it is ready for further configuration. Configuring the expectations on the server consists of calling one of the following methods:

ErsatzServer expectations(final Consumer<Expectations> expects)

ErsatzServer expectations(@DelegatesTo(Expectations) final Closure closure)

Expectations expects()

The first allows for configuration within a Consumer<Expectations> instance, which will have a prepared Expectations instance passed into it. This allows for a DSL-style configuration from Java.

The second method is the entry point for the Groovy DSL configuration. The provided Closure will delegate to an instance of Expectations for defining the configurations.

The third method is a simplified builder-style approach for single request method expectation-building.

Once the request expectations are configured, the server must be started by calling the ErsatzServer::start() method. This will start the underlying embedded HTTP server and register the configured expectations. If the server is not started, you will receive connection errors during testing.

Tip
You can set the server to start automatically after expectations are applied by adding the autoStart() configuration to the global configuration closure.

Further details about configuration options and examples can be found in the Configuration section of this user guide.

Testing

After configuration, the server is running and ready for test interactions. Any HTTP client can make HTTP requests against the server to retrieve configured responses. The ErsatzServer object provides helper methods to retrieve the server port and URL, with getHttpPort() and getHttpUrl() respectively (there are also versions for HTTPS, getHttpsPort() and getHttpsUrl() respectively). Note that the server will always be started on an ephemeral port so that a random one will be chosen to avoid collisions.

Verification

Once testing has been performed, it may be desirable to verify whether or not the expected number of request calls were matched. The Expectations interface provides a called method to add call count verification per configured request, something like:

post('/user').body(content, 'application/json').called(1)
    .responds().content(successContent, 'application/json')

This would match a POST request to /user with request body content matching the provided content and expect that matched call only once. When verify() is called it will return true if this request has only been matched once, otherwise it will return false. This allows testing to ensure that requests are not made more often than expected or at unexpected times.

Verification is optional and may simply be skipped if not needed.

Cleanup

After testing and verification, when all test interactions have completed, the server must be stopped in order to free up resources. This is done by calling the ErsatzServer::stop() method. This is an important step, as odd test failures have been noticed during multi-test runs if the server is not properly stopped. In Spock you can create the ErsatzServer with the @AutoCleanup annotation to aid in proper management:

@AutoCleanup('stop') ErsatzServer server = new ErsatzServer()

likewise, in a JUnit test (Groovy or Java) you may use the ErsatzServerRule class, which is a JUnit Rule implementation delegating to an ErsatzServer; it automatically calls the stop() method after each test method, though the start() method must still be called manually.

@Rule ErsatzServerRule ersatzServer = new ErsatzServerRule()

@Test public void hello(){
    ersatzServer.expectations(expectations -> {
        expectations.get("/testing").responds().content("ok");
    }).start();

    okhttp3.Response response = new OkHttpClient().newCall(
        new Request.Builder().url(format("%s/testing", ersatzServer.getHttpUrl())).build()
    ).execute();

    assertEquals(200, response.code());
    assertEquals("ok", response.body().string());
}

The server may be restarted after it has been stopped; however, be aware that expectation configuration is additive and existing configuration will remain on server start even if new expectations are configured.

Configuration

The ServerConfig interface provides the configuration methods for the server, both in the constructor and on the server instance itself. In most cases, there is no difference in functionality (save where noted).

Authentication

The Ersatz server supports two forms of built-in authentication, BASIC and DIGEST. Both authentication methods are exclusive and global, meaning that they cannot be configured together on the same server and that when configured, they apply to all end points configured on the server.

If more fine-grained control of which URLs are authenticated is desired, you will need to configured multiple Ersatz Servers for the different configuration sets.

BASIC Authentication

HTTP BASIC Authentication is supported by applying the basic authentication configuration to the server.

def ersatz = new ErsatzServer({
    authentication {
        basic 'admin', 'my-password'
    }
})

This configuration causes the configured request expectations to require BASIC authentication (username and password) as part of their matching.

DIGEST Authentication

HTTP DIGEST Authentication is supported by applying the digest authentication to the server.

def ersatz = new ErsatzServer({
    authentication {
        digest 'guest', 'other-password'
    }
})

This configuration causes the configured request expectations to require DIGEST authentication (username and password) as part of their matching.

Auto-Start

An auto-start feature is provided to allow the server to start automatically once expectations have been applied (e.g. after the expectations() method is called. This can simplify the code by removing explicit calls to the start() method. There are two configuration methods for the auto-start feature. The first one, is simply a means of enabling auto-start:

def ersatz = new ErsatzServer({
    autoStart()
})

while the second provides a way to toggle the feature on (true) or off (false):

def ersatz = new ErsatzServer({
    autoStart true
})

This toggling capability allows for an external configuration source to determine whether or not auto-start is enabled.

Tip
While the auto-start methods may be used in the constructor or instance configuration, it is generally meant for use in the constructor, as a global configuration.

Content Translation

The translation of request/response body content between types is performed using:

  • Decoders to convert incoming request body content into an expected comparison type

  • Encoders to convert outgoing response body configuration types into HTTP string data

The request/response content body decoders/encoders are configured in a layered manner so that they may be configured and shared across multiple instances without copying the configuration.

  • Encoders/Decoders configured in the ErsatzServer constructor are considered "global" and will be used if no overriding handlers are configured.

  • Encoders/Decoders configured in the request/response itself are considered "local" and will override any other configured handlers

  • Other configurations are applied in a layered order based on where they are applied in the configuration DSL - the handlers are maintained as separate isolated instances and the actual handler is resolved at runtime.

The specifics of Decoders and Encoders are discussed in the following sections.

Decoders

The Decoders are used to convert request content bytes into a specified configuration type for matching in Ersatz. Decoders are implemented as a BiFunction<byte[],DecodingContext, Object>', which takes a `byte array of request content and converts it to a specific Object type. The DecodingContext is used to provide additional information about the request being decoded (e.g. contentLength, contentType, characterEncoding, and a reference to the decoderChain).

The various configuration levels have the same method signature:

ServerConfig decoder(final String contentType, final BiFunction<byte[], DecodingContext, Object> decoder)

As an example, the default JSON decoder (provided in com.stehno.ersatz.Decoders) looks like the following code:

static final BiFunction<byte[], DecodingContext, Object> parseJson = { byte[] content, DecodingContext ctx ->
    new JsonSlurper().parse(content ?: '{}'.bytes)
}

Likewise, in Groovy, you can provide a Closure instead of a BiFunction, as long as it provides the same expected inputs and outputs:

def server = new ErsatzServer({
    decoder('application/json'){ content, context ->
        new JsonSlurper().parse(content ?: '{}'.bytes)
    }
})

The two approaches are functionally the same.

Encoders

The Encoders are used to convert response configuration data types into the outbound request content string. They are implemented as a Function<Object,String> with the input Object being the configuration object being converted, and the String is the return type.

The various configuration levels have the same method signature:

ServerConfig encoder(String contentType, Class objectType, Function<Object, String> encoder)

The contentType is the response content type to be encoded and the objectType is the type of configuration object to be encoded - this allows for the same content-type to have different encoders for different configuration object types.

A simple example of an encoder would be the default JSON encoder (provided in the com.stehno.ersatz.Encoders class):

static final Function<Object, String> json = { obj -> obj != null ? toJson(obj) : '{}' }

You may also configure encoders as Groovy `Closure`s with the same parameters:

def server = new ErsatzServer({
    encoder('application/json',Map){ obj->
        obj != null ? toJson(obj) : '{}'
    }
})

The two approaches are functionally equivalent.

Expectations

Request expectations are the core of the Ersatz server functionality; conceptually, they are HTTP server request routes which are used to match an incoming HTTP request with a request handler or to respond with a status of 404, if no matching request was configured. The expectations are configured on an instance of the Expectations interface, which provides multiple configuration methods for each of the supported HTTP request methods (GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS, and TRACE), with the method name corresponding to the HTTP request method name. The four general types of methods are:

  • One taking a String path returning an instance of the Request interface

  • One taking a String path and a Consumer<Request> returning an instance of the Request interface

  • One taking a String path and a Groovy Closure returning an instance of the Request interface

  • All of the above with the String path replaced by a Hamcrest Matcher<String> for matching the path

The Consumer<Request> methods will provide a Consumer<Request> implementation to perform the configuration on a Request instance passed into the consumer function. The path strings in the verb methods may be called with as a wildcard value - this will match any request with that request method (e.g. get('') would match any GET request while any('*') could be used to match any request made on the server).

The Closure support is similar to that of the consumer; however, this is a Groovy DSL approach where the Closure operations are delegated onto the a Request instance in order to configure the request.

All of the expectation method types return an instance of the request being configured (Request or RequestWithContent).

There is also an any request method matcher configuration which will match a request regardless of the request method, if it matches the rest of the configured criteria.

The primary role of expectations is to provide a means of matching incoming requests in order to respond in a desired and repeatable manner. They are used to build up matching rules based on request properties to help filter and route the incoming request properly. Hamcrest Matcher support allows for flexible request matching based on various request properties.

The configuration interfaces support three main approaches to configuration, a chained builder approach, such as:

head('/foo')
    .query('a','42')
    .cookie('stamp','1234')
    .respond().header('ok','true')

where the code is a chain of builder-style method calls used to wire up the request expectation. The second method is available to users of the Groovy language, the Groovy DSL approach would code the same thing as:

head('/foo'){
    query 'a', '42'
    cookie 'stamp', '1234'
    responder {
        header 'ok', "true"
    }
}

which can be more expressive, especially when creating more complicated expectations. A third approach is a Java-based approach more similar to the Groovy DSL, using the Consumer<?> methods of the interface, this would yield:

head('/foo', req -> {
    req.query("a", "42")
    req.cookie("stamp", "1234")
    req.responder( res-> {
        res.header("ok", "true")
    })
})

Any of the three may be used in conjunction with each other to build up expectations in the desired manner.

Tip
The matching of expectations is perform in the order the expectations are configured, such that if an incoming request could be matched by more than one expectation, the first one configured will be applied.

Request expectations may be configured to respond differently based on how many times a request is matched, for example, if you wanted the first request of GET /something to respond with Hello and second (and all subsequent) request of the same URL to respond with Goodbye, you would configure multiple responses, in order:

get('/something'){
    responder {
        content 'Hello'
    }
    responder {
        content 'Goodbye'
    }
    called 2
}

Adding the called configuration adds the extra safety of ensuring that if the request is called more than our expected two times, the verification will fail (and with that, the test).

Expectations may be cleared from the server using the clearExpectations() method. This is useful when you need to redefine expectations for one test only, but all of the others have a common set of expectations.

Request Methods

The Ersatz server supports all of the standard HTTP request headers along with a few non-standard ones. The table below denotes the supported methods their contents.

Method

Request Body

Response Body

Reference

GET

N

Y

RFC2616 Sec 9.3

HEAD

N

N

RFC2616 Sec 9.4

OPTIONS

N

N

RFC2616 Sec 9.2

POST

Y

Y

RFC2616 Sec 9.5

PUT

Y

N

RFC2616 Sec 9.6

DELETE

N

N

RFC2616 Sec 9.7

PATCH

Y

N

RFC5789

TRACE

N

Y

RFC2616 Sec 9.8

The following sections describe how each method is supported with a simple example.

While Ersatz does constrain the content of the request and response based on the request method, it is generally up to the mocker to provide the desired and/or appropriate responses (including most headers). This implementation leniency is intentional, and is meant to allow for endpoint implementations that do not necessarily follow the published specification, but likewise still need to be tested as they really exist rather than how they should exist.

HEAD

A HEAD request is used to retrieve the headers for a URL, basically a GET request without any response body. An Ersatz mocking example would be:

ersatzServer.expectations {
    head('/something').responds().header('X-Alpha','Interesting-data').code(200)
}

which would respond to HEAD /something with an empty response and the response header X-Alpha with the specified value.

GET

The GET request is a common HTTP request, and what browsers do by default. It has no request body, but it does have response content. You mock GET requests using the get() methods, as follows:

ersatzServer.expectations {
    get('/something').responds().content('This is INTERESTING!', 'text/plain').code(200)
}

In a RESTful interface, a GET request is usually used to "read" or retrieve a resource representation.

OPTIONS

The OPTIONS HTTP request method is similar to an HEAD request, having no request or response body. The primary response value in an OPTIONS request is the content of the Allow response header, which will contain a comma-separated list of the request methods supported by the server. The request may be made against a specific URL path, or against * in order to determine what methods are available to the entire server.

In order to mock out an OPTIONS request, you will want to respond with a provided Allow header. This may be done using the Response.allows(HttpMethod…​) method in the responder. An example would be something like:

ersatzServer.expectations {
    options('/options').responds().allows(GET, POST).code(200)
    options('/*').responds().allows(DELETE, GET, OPTIONS).code(200)
}

This will provide different allowed options for /options and for the "entire server" (*). You can also specify the Allow header as a standard response header.

Note that not all client and servers will support the OPTIONS request method.

POST

The POST request is often used to send browser form data to a backend server. It can have both request and response content.

ersatzServer.expectations {
    post('/form'){
        body([first:'John', last:'Doe'], APPLICATION_URLENCODED)
        responder {
            content('{ status:'saved' }', APPLICATION_JSON)
        }
    }
}

In a RESTful interface, the POST method is generally used to "create" new resources.

PUT

A PUT request is similar to a POST except that while there is request content, there is no response body content.

ersatzServer.expectations {
    put('/form'){
        query('id','1234')
        body([middle:'Q'], APPLICATION_URLENCODED)
        responder {
            code(200)
        }
    }
}

In a RESTful interface, a PUT request if most often used as an "update" operation.

DELETE

A DELETE request has not request or response content. It would look something like:

ersatzServer.expectations {
    delete('/user').query('id','1234').responds().code(200)
}

In a RESTful interface, a DELETE request may be used as a "delete" operation.

PATCH

The PATCH request method creates a request that can have body content; however, the response will have no content.

ersatzServer.expectations {
    patch('/user'){
        query('id','1234')
        body('{ "middle":"Q"}', APPLICATION_JSON)
        responder {
            code(200)
        }
    }
}

In a RESTful interface, a PATCH request may be used as a "modify" operation for an existing resource.

TRACE

The TRACE method is generally meant for debugging and diagnostics. The request will have no request content; however, if the request is valid, the response will contain the entire request message in the entity-body, with a Content-Type of message/http. With that in mind, the TRACE method is implemented a bit differently than the other HTTP methods. It’s not available for mocking, but it will provide an echo of the request as it is supposed to. For example the following request (raw):

TRACE / HTTP/1.1
Host: www.something.com

would respond with something like the following response (raw):

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Tue, 31 Oct 2006 08:01:48 GMT
Connection: close
Content-Type: message/http
Content-Length: 39

TRACE / HTTP/1.1
Host: www.something.com

Since this functionality is already designed for diagnostics purposes, it was decided that it would be best to simply implement and support the request method rather than allow it to be mocked.

Making a TRACE request to Ersatz looks like the following:

ersatzServer.start()

URL url = new URL("${ersatzServer.httpUrl}/info?data=foo+bar")
HttpURLConnection connection = url.openConnection() as HttpURLConnection
connection.requestMethod = 'TRACE'

assert connection.contentType == MESSAGE_HTTP.value
assert connection.responseCode == 200

assert connection.inputStream.text.readLines()*.trim() == """TRACE /info?data=foo+barHTTP/1.1
    Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
    Connection: keep-alive
    User-Agent: Java/1.8.0_121
    Host: localhost:${ersatzServer.httpPort}
""".readLines()*.trim()

The explicit start() call is required since there are no expecations specified (auto-start wont fire). The HttpUrlConnection is used to make the request, and it can be seen that the response content is the same as the original request content.

The TRACE method is supported using the built-in HttpTraceHandler provided by the embedded Undertow server.

Note
At some point, if there are valid use cases for allowing mocks of TRACE it could be supported. Feel free to create an Issue ticket describing your use case and it will be addressed.

Request Matching

Hamcrest Matchers

Many of the expectation methods accept Hamcrest Matcher instances as an alternate argument. Hamcrest matchers allow for a more rich and expressive matching configuration. Consider the following configuration:

server.expectations {
    get( startsWith('/foo') ){
        called greaterThanOrEqualTo(2)
        query 'user-key', notNullValue()
        responder {
            content 'ok', TEXT_PLAIN
        }
    }
}

This configuration would match a GET request to a URL starting with /foo, with a non-null query string "user-key" value. This request matcher is expected to be called at least twice and it will respond with a text/plain response of ok.

The methods that accept matchers will have a non-matcher version which provides a sensible default matcher (e.g. get(Matcher) has get(String) which provides delegates to get( equalTo( string ) ) to wrap the provided path string in a matcher.

If you are using Groovy, you can actually replace Hamcrest matchers with a Closure emulating the same interface - basically a method that takes the parameter and returns whether or not the condition was matched. The same example above could be re-written as:

server.expectations {
    get({ p-> p.startsWith('/foo') }){
        called { i-> i >= 2 }
        query 'user-key', notNullValue()
        responder {
            content 'ok', TEXT_PLAIN
        }
    }
}

This allows for additional flexibility in configuring expectations.

Matching Cookies

There are four methods for matching cookies associated with a request (found in the com.stehno.ersatz.Request interface):

By Name and Matcher

The cookie(String name, Matcher<Cookie> matcher) method configures the specified matcher for the cookie with the given name.

server.expectations {
    get('/somewhere'){
        cookie 'user-key', CookieMatcher.cookieMatcher {
            value startsWith('key-')
            domain 'mydomain.com'
        }
        responds().code(200)
    }
}

The Hamcrest matcher used may be a custom Matcher implementation, or the provided com.stehno.ersatz.CookieMatcher.

By Name and Value

The cookie(String name, String value) method is a shortcut for configuring simple name/value matching where the cookie value must be equal to the specified value. An example:

server.expectations {
    get('/somewhere').cookie('user-key', 'key-23435HJKSDGF86').responds().code(200)
}

This is equivalent to calling the matcher-based version of the method:

server.expectations {
    get('/somewhere'){
        cookie 'user-key', CookieMatcher.cookieMatcher {
            value equalTo('key-23435HJKSDGF86')
        }
        responds().code(200)
    }
}
Multiple Cookies

The cookies(Map<String,Object>) method provides a means of configuring multiple cookie matchers (as value `String`s or cookie `Matcher`s). In the following example matchers are configured to match the 'user-key' cookie for values "starting with" the specified value, the request should also have an 'app-id' cookie with a value of "user-manager", and finally the request should not have the 'timeout' cookie specified.

server.expectations {
    get('/something'){
        cookies([
            'user-key': cookieMatcher {
                value startsWith('key-')
            },
            'appid': 'user-manager',
            'timeout': nullValue()
        ])
        responds().code(200)
    }
}

Overall Matcher

The cookies(Matcher<Map<String,Cookie>) method is used to specify a Matcher for the map of cookie names to com.stehno.ersatz.Cookie objects. The matcher may be any custom matcher, or the com.stehno.ersatz.NoCookiesMatcher may be used to match for the case where no cookies should be defined in the request:

server.expectations {
    get('/something'){
        cookies NoCookiesMatcher.noCookies()
        responds().code(200)
    }
}

HTTPS

The ErsatzServer supports HTTPS requests when the https() configuration is set (either as https() or as https true). This will setup both an HTTP and HTTPS listener both of which will have access to all configured expectations. In order to limit a specific request expectation to HTTP or HTTPS, apply the procotol(String) matcher method with the desired protocol, for example:

server.expectations {
    get('/something').protocol('https').responding('thing')
}

which will match an HTTPS request to GET /something and send a response of thing.

Note
the HTTPS support is rudimentary and meant to test HTTPS endpoints, not any explicit features of HTTPS itself. Also your client will need to be able to ignore any self-signed certification issues in one way or another.

Creating a Custom Keystore

A default keystore is provided with the Ersatz library, and it should suffice for most cases; however, you may wish to provide your own custom keystore for whatever reason. A supported keystore file may be created using the following command:

./keytool -genkey -alias <NAME> -keyalg RSA -keystore <FILE_LOCATION>

where <NAME> is the key name and <FILE_LOCATION> is the location where the keystore file is to be created. You will be asked a few questions about the key being created. The default keystore name is ersatz and it has the following properties:

CN=Ersatz, OU=Ersatz, O=Ersatz, L=Nowhere, ST=Nowhere, C=US

Obviously, it is only for testing purposes.

The keystore should then be provided during server configuration as:

ErsatzServer server = new ErsatzServer({
    https()
    keystore KEYSTORE_URL, KEYSTORE_PASS
})

where KEYSTORE_URL is the URL to your custom keystore file, and KEYSTORE_PASS is the password (maybe omitted if you used ersatz as the password).

Request / Response Compression

Ersatz supports GZip and Deflate compression seamlessly as long as the Accept-Encoding header is specified as gzip or deflate. If the response is compressed, a Content-Encoding header will be added to the response with the appropriate compression type as the value.

Multipart Request Content

Ersatz server supports multipart file upload requests (multipart/form-data content-type) using the Apache File Upload library on the "server" side. The expectations for multipart requests are configured using the MultipartRequestContent class to build up an equivalent multipart matcher:

ersatz.expectataions {
    post('/upload') {
        decoders decoders
        decoder MULTIPART_MIXED, Decoders.multipart
        decoder IMAGE_PNG, Decoders.passthrough
        body multipart {
            part 'something', 'interesting'
            part 'infoFile', 'info.txt', TEXT_PLAIN, infoText
            part 'imageFile', 'image.png', IMAGE_PNG, imageBytes
        }, MULTIPART_MIXED
        responder {
            content 'ok'
        }
    }
}

which will need to exactly match the incoming request body in order to be considered a match. There is also a MultipartRequestMatcher used to provide a more flexible Hamcrest-based matching of the request body:

server.expectations {
    post('/upload') {
        decoders decoders
        decoder MULTIPART_MIXED, Decoders.multipart
        decoder IMAGE_PNG, Decoders.passthrough
        body multipartMatcher {
            part 'something', notNullValue()
            part 'infoFile', endsWith('.txt'), TEXT_PLAIN, notNullValue()
            part 'imageFile', endsWith('.png'), IMAGE_PNG, notNullValue()
        }, MULTIPART_MIXED
        responder {
            content 'ok'
        }
    }
}

This will configure a match of the request body content based on the individual matchers, rather than overall equivalence.

A key point in multipart request support are the "decoders", which are used to decode the incoming request content into an expected object type. Decoders are simply BiFunction<byte[], DecodingContext, Object> implementations - taking the incoming byte array, and a DecodingContext and returning the decoded Object instance. Decoders may be registered in a shared instance of RequestDecoders, configured globally across the server instance or configured on a per-request basis.

Tip
No decoders are provided by default, any used in the request content must be provided in configuration.

Some common reusable decoders are provided in the Decoders utility class.

Multipart Response Content

Multipart response content is supported, though most browsers do not fully support it - the expected use case would be a RESTful or other HTTP-based API. The response content will have the standard multipart/form-data content type and format. The response content parts are provided using an instance of the MultipartResponseContent class along with the Encoders.multipart multipart response content encoder (configured on the server or response).

The content parts are provided as "field" parts with only a field name and value, or as "file" parts with a field name, content-type, file name and content object. These configurations are made on the MultipartResponseContent object via DSL or functional interface.

The part content objects are serialized for data transfer as String content using configured encoders, which are simply instances of Function<Object,String> used to do the object to string conversion. These are configured either on a per-response basis or by sharing a ResponseEncoders instance between multipart configurations - the shared encoders will be used if not explicitly overridden by the multipart response configuration. No part encoders are provided by default.

An example multipart response with a field and an image file would be something like:

ersatz.expectations {
    get('/data') {
        responder {
            encoder ContentType.MULTIPART_MIXED, MultipartResponseContent, Encoders.multipart
            content(multipart {
                // configure the part encoders
                encoder TEXT_PLAIN, CharSequence, { o -> o as String }
                encoder IMAGE_JPG, File, { o -> ((File)o).bytes.encodeBase64() }

                // a field part
                field 'comments', 'This is a cool image.'

                // a file part
                part 'image', 'test-image.jpg', IMAGE_JPG, new File('/test-image.jpg'), 'base64'
            })
        }
    }
}

The resulting response body would look like the following (as a String):

--WyAJDTEVlYgGjdI13o
Content-Disposition: form-data; name="comments"
Content-Type: text/plain

This is a cool image.
--WyAJDTEVlYgGjdI13o
Content-Disposition: form-data; name="image"; filename="test-image.jpg"
Content-Transfer-Encoding: base64
Content-Type: image/jpeg

... more content follows ...

which could be decoded in the same manner a multipart request content (an example using the Apache File Upload multipart parser can be found in the unit tests).

Usage Examples

This section contains some recipe-style usage examples.

Url-Encoded Form Requests

Url-encoded form requests are supported by default when the request content-type is specified as application/x-www-form-urlencoded. The request body expectation configuration will expect a Map<String,String> equivalent to the name-value pairs specified in the request body content. An example would be:

server.expectations {
    post('/form') {
        body([alpha: 'some data', bravo: '42'], 'application/x-www-form-urlencoded')
        responder {
            content 'ok'
        }
    }
}

where the POST content data would look like:

alpha=some+data&bravo=42

File Upload (POST)

You can setup an expectation for a file upload POST using the multipart support, something like:

import com.stehno.erstaz.ErsatzServer
import com.stehno.ersatz.MultipartRequestContent
import static com.stehno.ersatz.ContentType.TEXT_PLAIN

def ersatz = new ErsatzServer({
    autoStart()
    encoder TEXT_PLAIN, File, Encoders.text
})

def file = new File(/* some file */)

ersatz.expectations {
    post('/upload') {
        decoders TEXT_PLAIN, Decoders.utf8String
        decoder MULTIPART_MIXED, Decoders.multipart

        body MultipartRequestContent.multipart {
            part 'fileName', file.name
            part 'file', file.name, 'text/plain; charset=utf-8', file.text
        }, MULTIPART_MIXED

        responder {
            content 'ok'
        }
    }
}

This will expect the posting of the given file content to the /upload path of the server.

File Download (GET)

Setting up an expectation for a GET request to respond with a file to download can be done as follows:

import com.stehno.erstaz.ErsatzServer
import static com.stehno.ersatz.ContentType.TEXT_PLAIN

def ersatz = new ErsatzServer({
    autoStart()
    encoder TEXT_PLAIN, File, Encoders.text
})

def file = new File(/* some file */)

ersatz.expectations {
    get('/download'){
        responder {
            header 'Content-Disposition', "attachment; filename=\"${file.name}\""
            content file, TEXT_PLAIN
        }
    }
}

This will respond to the request with file download content.

Kotlin Usage

You can use the Ersatz Server from the Kotlin programming language just as easily as Java or Groovy:

val ersatz = ErsatzServer { config -> config.autoStart() }

ersatz.expectations { expectations ->
    expectations.get("/kotlin").called(1).responder { response ->
        response.content("Hello Kotlin!", ContentType.TEXT_PLAIN).code(200)
    }
}

val http = OkHttpClient.Builder().build()
val request: okhttp3.Request = okhttp3.Request.Builder().url("${ersatz.httpUrl}/kotlin").build()
println( http.newCall(request).execute().body().string() )

which will print out "Hello Kotlin!" when executed.