File: README.md
[Moco](https://github.com/dreamhead/moco)
Moco is an easy setup stub framework.
Latest Release
Note: Moco 2.0.0 requires Java 17 or later.
User Voice
- Let me know if you are using Moco.
- Join Moco mailing list to discuss.
Why
Integration, especially based on HTTP protocol, e.g. web service, REST etc, is wildly used in most of our development.
In the old days, we just deployed another WAR to an application server, e.g. Jetty or Tomcat etc. As we all know, it's so boring to develop a WAR and deploy it to any application server, even if we use an embeded server. And the WAR needs to be reassembled even if we just want to change a little bit.
Quick Start
- Download Standalone Moco Runner
- Write your own configuration file to describe your Moco server configuration as follow:
[
{
"response" :
{
"text" : "Hello, Moco"
}
}
](foo.json)
- Run Moco HTTP server with the configuration file.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json- Now, open your favorite browser to visit http://localhost:12306 and you will see "Hello, Moco".
Documents
- More Usages
- Detailed HTTP APIs
- Detailed REST API
- Detailed Websocket API
- Detailed SSE API
- Global Settings for multiple configuration files.
- Command Line Usages
- Extend Moco if current API does not meet your requirement.
Build
Requirements: JDK 17+ and Gradle
- Clone Moco
git clone [email protected]:dreamhead/moco.git- Build Moco
./gradlew build- Build uberjar
./gradlew uberjar- Check code before commit
./gradlew checkContributing
Check out what you can help here if you do not have any existing idea.
Copyright and license
Copyright 2012-2026 ZHENG Ye
Licensed under MIT License (the "License"); You may obtain a copy of the License in MIT-LICENSE.txt, or at:
https://raw.github.com/dreamhead/moco/master/MIT-LICENSE.txt
Powered By
Star History
File: MIT-LICENSE.txt
The MIT License (MIT)
Copyright (c) 2012-2026 ZHENG Ye
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File: moco-core/src/test/resources/gbk.json
[Binary file]
File: moco-core/src/test/resources/details/bar.json
[
{
"request" : {
"uri" : "/bar"
},
"response" : {
"text" : "bar"
}
}
]
File: moco-core/src/test/resources/details/foo.json
[
{
"request" : {
"uri" : "/foo"
},
"response" : {
"text" : "foo"
}
}
]
File: moco-doc/apis.md
HTTP(s) APIs
Moco mainly focuses on server configuration. There are only two kinds of API right now: Request and Response.
That means if we get the expected request and then return our response. Now, you can see a Moco reference in details.
WARNING the json configuration below is just a snippet for one pair of request and response, instead of the whole configuration file.
Table of Contents
- Composite Java API Design
- Description as comment
- Request
- Response
- Content
- Status Code
- Version
- Header
- Proxy
- Redirect
- Cookie
- CORS
- Default All CORS
- CORS with allowOrigin/Access-Control-Allow-Origin
- CORS with allowMethods/Access-Control-Allow-Methods
- CORS with allowHeaders/Access-Control-Allow-Headers
- CORS with maxAge/Access-Control-Max-Age
- CORS with allowCredentials/Access-Control-Allow-Credentials
- CORS with exposeHeaders/Access-Control-Expose-Headers
- Attachment
- Latency
- Sequence
- Cycle
- JSON Response
- Mount
- Template
- Record and Replay
- Event
- Verify
- Miscellaneous
Composite Java API Design
Moco Java API is designed in functional fashion which means you can composite any request or response easily.
server.request(and(by(uri("/target")), by(version(VERSION_1_0)))).response(with(text("foo")), header("Content-Type", "text/html"));Description as comment
@Since 0.7
In all JSON APIs, you can use description to describe what is this session for. It's just used as comment, which will be ignored in runtime.
[
{
"description": "any response",
"response": {
"text": "foo"
}
}
]Request
Content
@Since 0.7
If you want to response according to request content, Moco server can be configured as following:
- Java API
server.request(by("foo")).response("bar");- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"text" : "bar"
}
}If request content is too large, you can put it in a file:
- Java API
server.request(by(file("foo.request"))).response("bar");- JSON
{
"request" :
{
"file" : "foo.request"
},
"response" :
{
"text" : "bar"
}
}@Since 1.2.0
You can also match binary request directly.
server.request(binary(new byte[] {1, 2, 3})).response("bar");InputStream is also supported in binary.
server.request(binary(new ByteArrayInputStream(new byte[]{1, 2, 3}))).response("bar");URI
@Since 0.7
If request uri is your major focus, Moco server could be like this:
- Java API
server.request(by(uri("/foo"))).response("bar");- JSON
{
"request" :
{
"uri" : "/foo"
},
"response" :
{
"text" : "bar"
}
}Query Parameter
@Since 0.7
Sometimes, your request has parameters in query:
- Java API
server.request(and(by(uri("/foo")), eq(query("param"), "blah"))).response("bar");- JSON
{
"request" :
{
"uri" : "/foo",
"queries" :
{
"param" : "blah"
}
},
"response" :
{
"text" : "bar"
}
}HTTP Method
@Since 0.7
It's easy to response based on specified HTTP method:
- Java API
server.get(by(uri("/foo"))).response("bar");- JSON
{
"request" :
{
"method" : "get",
"uri" : "/foo"
},
"response" :
{
"text" : "bar"
}
}Also for POST method:
- Java API
server.post(by("foo")).response("bar");- JSON
{
"request" :
{
"method" : "post",
"text" : "foo"
},
"response" :
{
"text" : "bar"
}
}and PUT method:
- Java API
server.put(by("foo")).response("bar");- JSON
{
"request" :
{
"method" : "put",
"text" : "foo"
},
"response" :
{
"text" : "bar"
}
}and DELETE method:
- Java API
server.delete(by(uri("/foo"))).response(status(200));- JSON
{
"request" :
{
"method" : "delete",
"uri" : "/foo"
},
"response" :
{
"status" : "200"
}
}If you do need other method, feel free to specify method directly:
- Java API
server.request(by(method("HEAD"))).response("bar");- JSON
{
"request" :
{
"method" : "HEAD"
},
"response" :
{
"text" : "bar"
}
}Version
@Since 0.7
We can return different response for different HTTP version:
- Java API
server.request(by(version("HTTP/1.0"))).response("version");- JSON
{
"request":
{
"version": "HTTP/1.0"
},
"response":
{
"text": "version"
}
}Header
@Since 0.7
We will focus HTTP header at times:
- Java API
server.request(eq(header("foo"), "bar")).response("blah")- JSON
{
"request" :
{
"method" : "post",
"headers" :
{
"content-type" : "application/json"
}
},
"response" :
{
"text" : "bar"
}
}Cookie
@Since 0.7
Cookie is widely used in web development.
- Java API
server.request(eq(cookie("loggedIn"), "true")).response(status(200));- JSON
{
"request" :
{
"uri" : "/cookie",
"cookies" :
{
"login" : "true"
}
},
"response" :
{
"text" : "success"
}
}Form
@Since 0.7
In web development, form is often used to submit information to server side.
- Java API
server.post(eq(form("name"), "foo")).response("bar");- JSON
{
"request" :
{
"method" : "post",
"forms" :
{
"name" : "foo"
}
},
"response" :
{
"text" : "bar"
}
}XML
@Since 0.7
XML is a popular format for Web Services. When a request is in XML, only the XML structure is important in most cases and whitespace can be ignored. The xml operator can be used for this case.
- Java API
server.request(xml(text("<request><parameters><id>1</id></parameters></request>"))).response("foo");- JSON
{
"request":
{
"uri": "/xml",
"text":
{
"xml": "<request><parameters><id>1</id></parameters></request>"
}
},
"response":
{
"text": "foo"
}
}NOTE: Please escape the quote in text.
The large request can be put into a file:
{
"request":
{
"uri": "/xml",
"file":
{
"xml": "your_file.xml"
}
},
"response":
{
"text": "foo"
}
}XPath
@Since 0.7
For the XML/HTML request, Moco allows us to match request with XPath.
- Java API
server.request(eq(xpath("/request/parameters/id/text()"), "1")).response("bar");- JSON
{
"request" :
{
"method" : "post",
"xpaths" :
{
"/request/parameters/id/text()" : "1"
}
},
"response" :
{
"text" : "bar"
}
}XML Struct
@Since 1.3.0
Moco also allows you to match an XML request only for same struct no matter what actual content is.
- Java API
server.request(struct(xml("<foo>1</foo>")).response("response_for_xml_struct_request");- JSON
{
"request":
{
"struct":
{
"xml" : "<foo>1</foo>"
}
},
"response":
{
"text": "response_for_xml_struct_request"
}
}JSON Request
JSON Text
@Since 0.7
Json is rising with RESTful style architecture. Just like XML, in the most case, only JSON structure is important, so json operator can be used.
- Java API
server.request(json(text("{\"foo\":\"bar\"}"))).response("foo");@Since 0.12.0json will return a resource from 1.3.0.
server.request(by(json(text("{\"foo\":\"bar\"}")))).response("foo");Note that this functionality is implemented in Jackson, please make sure your POJO is written in Jackson acceptable format.
- JSON
{
"request":
{
"uri": "/json",
"text":
{
"json": "{\"foo\":\"bar\"}"
}
},
"response":
{
"text": "foo"
}
}NOTE: Please escape the quote in text.
JSON Shortcut
If the response is JSON, we don't need to write JSON text with escape character in code.
@Since 0.10.2
You can give a POJO to Java API, it will be converted JSON text.
server.request(json(pojo)).response("foo");@Since 0.12.0json will return a resource from 1.3.0.
server.request(by(json(pojo))).response("foo");@Since 0.9.2
As you have seen, it is so boring to write json with escape character, especially in json configuration. So you can try the json shortcut. The upper case could be rewritten as following:
- JSON
{
"request": {
"uri": "/json",
"json": {
"foo": "bar"
}
},
"response": {
"text": "foo"
}
}JSON File
@Since 0.7
The large request can be put into a file:
- Java API
server.request(json(file("your_file.json"))).response("foo");- JSON
{
"request":
{
"uri": "/json",
"file":
{
"json": "your_file.json"
}
},
"response":
{
"text": "foo"
}
}JSONPath
@Since 0.7
For the JSON request, Moco allows us to match request with JSONPath.
- Java API
server.request(eq(jsonPath("$.book[*].price"), "1")).response("response_for_json_path_request");- JSON
{
"request":
{
"json_paths":
{
"$.book[*].price": "1"
}
},
"response":
{
"text": "response_for_json_path_request"
}
}JSON Struct
@Since 1.3.0
Moco also allows you to match a JSON request only for same struct no matter what actual content is.
- Java API
server.request(struct(json("{\"foo\":1}")).response("response_for_json_struct_request");- JSON
{
"request":
{
"struct":
{
"json" : {
"foo" :1
}
}
},
"response":
{
"text": "response_for_json_struct_request"
}
}Operator
Moco also supports some operators which helps you write your expectation easily.
Match
@Since 0.7
You may want to match your request with regular expression, match could be your helper:
- Java API
server.request(match(uri("/\\w*/foo"))).response("bar");- JSON
{
"request":
{
"uri":
{
"match": "/\\w*/foo"
}
},
"response":
{
"text": "bar"
}
}Moco is implemented by Java regular expression, you can refer here for more details.
Starts With
@Since 0.9.2
starsWith operator can help you decide if the request information starts with a piece of text.
- Java API
server.request(startsWith(uri("/foo"))).response("bar");- JSON
{
"request":
{
"uri":
{
"startsWith": "/foo"
}
},
"response":
{
"text": "bar"
}
}Ends With
@Since 0.9.2
endsWith operator can help you decide if the request information ends with a piece of text.
- Java API
server.request(endsWith(uri("foo"))).response("bar");- JSON
{
"request":
{
"uri":
{
"endsWith": "foo"
}
},
"response":
{
"text": "bar"
}
}Contain
@Since 0.9.2
contain operator helps you know whether the request information contains a piece of text.
- Java API
server.request(contain(uri("foo"))).response("bar");- JSON
{
"request":
{
"uri":
{
"contain": "foo"
}
},
"response":
{
"text": "bar"
}
}Exist
@Since 0.9.2
exist operator is used to decide whether the request information exists.
- Java API
server.request(exist(header("foo"))).response("bar");- JSON
{
"request":
{
"headers": {
"foo": {
"exist" : "true"
}
}
},
"response":
{
"text": "bar"
}
}For JSON API, you can decide whether the information does not exist.
- JSON
{
"request":
{
"headers": {
"foo": {
"exist" : "false"
}
}
},
"response":
{
"text": "bar"
}
}Path
@Since 1.4.0
path operator is provided to match uri with path variable.
- Java API
server.request(path(uri("/path/{path}/sub/{sub}"))).response("bar");- JSON
{
"request":
{
"uri":
{
"path": "/path/{path}/sub/{sub}"
}
},
"response":
{
"text": "sub"
}
}Conditional
@Since 1.3.0
If you want to implement your own matcher, you can write with conditional API which is supported in Java code.
server.request(conditional(request -> request.getContent().toString().equals("foo"))).response("foo");Response
Content
@Since 0.7
As you have seen in previous example, response with content is pretty easy.
- Java API
server.request(by("foo")).response("bar");- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"text" : "bar"
}
}The same as request, you can response with a file if content is too large to put in a string.
- Java API
server.request(by("foo")).response(file("bar.response"));- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"file" : "bar.response"
}
}@Since 0.10.1
You can specify file charset if you want to see it in correct encoding in console.
- Java API
server.response(file("src/test/resources/gbk.response", Charset.forName("GBK")));- JSON
[
{
"response":
{
"file":
{
"name": "gbk.response",
"charset": "GBK"
}
}
}
]Charset can also be used in path resource.
- Java API
server.response(pathResource("src/test/resources/gbk.response", Charset.forName("GBK")));- JSON
[
{
"response":
{
"path_resource":
{
"name": "gbk.response",
"charset": "GBK"
}
}
}
]@Since 1.2.0
You can also write binary response directly.
server.response(binary(new byte[] {1, 2, 3}));@Since 1.2.0
If your response need to be transformed for some reason, e.g. encryption, you can transform your content.
server.response(text("hello").transform(raw -> {
byte[] transformed = new byte[raw.length];
for (int i = 0; i < raw.length; i++) {
transformed[i] = (byte) (raw[i] + 1);
}
return transformed;
}));@Since 1.2.0
If you need your own specific response content, you can write your own code with lambda.
server.response(text((request) -> "foo"));server.response(binary((request) -> new byte[] {1, 2, 3}));Currently, this function uses com.github.dreamhead.moco.Request as argument which provide content only. If you need HTTP information in HTTP scenario, you can cast request to com.github.dreamhead.moco.HttpRequest.
server.response(text((request) -> ((HttpRequest)request).getUri()));Status Code
@Since 0.7
Moco also supports HTTP status code response.
- Java API
server.request(by("foo")).response(status(200));- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"status" : 200
}
}Version
@Since 0.7
By default, response HTTP version is supposed to request HTTP version, but you can set your own HTTP version:
- Java API
server.response(version(HttpProtocolVersion.VERSION_1_0));- JSON
{
"request":
{
"uri": "/version10"
},
"response":
{
"version": "HTTP/1.0"
}
}Header
@Since 0.7
We can also specify HTTP header in response.
- Java API
server.request(by("foo")).response(header("content-type", "application/json"));- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"headers" :
{
"content-type" : "application/json"
}
}
}Proxy
Single URL
@Since 0.8
We can also response with the specified url, just like a proxy.
- Java API
server.request(by("foo")).response(proxy("http://www.github.com"));- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"proxy" : "http://www.github.com"
}
}Actually, proxy is more powerful than that. It can forward the whole request to the target url, including HTTP method, version, header, content etc.
Failover
@Since 0.7
Besides the basic functionality, proxy also support failover, which means if remote server is not available temporarily, the server will know recovery from local configuration.
- Java API
server.request(by("foo")).response(proxy("http://www.github.com", failover("failover.json")));- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"proxy" :
{
"url" : "http://localhost:12306/unknown",
"failover" : "failover.json"
}
}
}Proxy will save request/response pair into your failover file. If the proxy target is not reachable, proxy will failover from the file. This feature is very useful for development environment, especially for the case the integration server is not stable.
As the file suffix suggests, this failover file is actually a JSON file, which means we can read/edit it to return whatever we want.
Playback
@Since 0.9.1
Moco also supports playback which also save remote request and response into local file. The difference between failover and playback is that playback only accesses remote server when local request and response are not available.
- Java API
server.request(by("foo")).response(proxy("http://www.github.com", playback("playback.json")));- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"proxy" :
{
"url" : "http://localhost:12306/unknown",
"playback" : "playback.json"
}
}
}Customize Failover/Playback Status
@Since 1.0.0
You can customize what remote statuses means that remote server is not available.
- Java API
server.request(by("foo")).response(proxy("http://www.github.com", failover("failover.json", 400, 500)));- JSON
{
"request" :
{
"text": "foo"
},
"response" :
{
"proxy" :
{
"url" : "http://www.github.com",
"failover" : {
"file": "failover.json",
"status": [404, 500]
}
}
}
}Batch URLs
@Since 0.9.1
If we want to proxy with a batch of URLs in the same context, proxy can also help us.
- Java API
server.get(match(uri("/proxy/.*"))).response(proxy(from("/proxy").to("http://localhost:12306/target")));- JSON
{
"request" :
{
"uri" : {
"match" : "/proxy/.*"
}
},
"response" :
{
"proxy" : {
"from" : "/proxy",
"to" : "http://localhost:12306/target"
}
}
}Same with single url, you can also specify a failover.
- Java API
server.request(match(uri("/proxy/.*")))
.response(proxy("http://localhost:12306/unknown"), failover("failover.response")));- JSON
{
"request" :
{
"uri" : {
"match" : "/failover/.*"
}
},
"response" :
{
"proxy" :
{
"from" : "/failover",
"to" : "http://localhost:12306/unknown",
"failover" : "failover.response"
}
}
}and playback.
- Java API
server.request(match(uri("/proxy/.*")))
.response(proxy("http://localhost:12306/unknown"), playback("playback.response")));- JSON
{
"request" :
{
"uri" : {
"match" : "/failover/.*"
}
},
"response" :
{
"proxy" :
{
"from" : "/failover",
"to" : "http://localhost:12306/unknown",
"playback" : "playback.response"
}
}
}As you may find, we often set request match same context with response, so Moco gives us a shortcut to do that.
- Java API
server.proxy(from("/proxy").to("http://localhost:12306/target"));- JSON
{
"proxy" : {
"from" : "/proxy",
"to" : "http://localhost:12306/target"
}
}Same with failover
Java API
javaserver.proxy(from("/proxy").to("http://localhost:12306/unknown"), failover("failover.response"));JSON
json{ "proxy" : { "from" : "/failover", "to" : "http://localhost:12306/unknown", "failover" : "failover.response" } }
and playback
Java API
javaserver.proxy(from("/proxy").to("http://localhost:12306/unknown"), playback("playback.response"));JSON
json{ "proxy" : { "from" : "/failover", "to" : "http://localhost:12306/unknown", "playback" : "playback.response" } }
Redirect
@Since 0.7
Redirect is a common case for normal web development. We can simply redirect a request to different url.
- Java API
server.get(by(uri("/redirect"))).redirectTo("http://www.github.com");- JSON
{
"request" :
{
"uri" : "/redirect"
},
"redirectTo" : "http://www.github.com"
}Cookie
@Since 0.7
Cookie can also be in the response.
- Java API
server.response(cookie("loggedIn", "true"), status(302));- JSON
{
"request" :
{
"uri" : "/cookie"
},
"response" :
{
"cookies" :
{
"login" : "true"
}
}
}Cookie Attributes
Cookie attributes are sent in http response, which are used by browsers to determine when to delete a cookie, block a cookie or whether to send a cookie to the server.
Path
@Since 0.11.1
Path cookie attribute defines the scope of the cookie. You can add your own path cookie attribute to your response.
- Java
server.response(cookie("loggedIn", "true", path("/")), status(302));- JSON
{
"request" :
{
"uri" : "/cookie"
},
"response" :
{
"cookies" :
{
"login" : {
"value" : "true",
"path" : "/"
}
}
}
}Domain
@Since 0.11.1
Domain cookie attribute defines the scope of the cookie. You can add your own domain cookie attribute to your response.
- Java
server.response(cookie("loggedIn", "true", domain("github.com")), status(302));- JSON
{
"request" :
{
"uri" : "/cookie"
},
"response" :
{
"cookies" :
{
"login" : {
"value" : "true",
"domain" : "github.com"
}
}
}
}Secure
@Since 0.11.1
A secure cookie can only be transmitted over an encrypted connection. You can add your own secure cookie attribute to your response.
- Java
server.response(cookie("loggedIn", "true", secure()), status(302));- JSON
{
"request" :
{
"uri" : "/cookie"
},
"response" :
{
"cookies" :
{
"login" : {
"value" : "true",
"secure" : "true"
}
}
}
}HTTP Only
@Since 0.11.1
An http only cookie cannot be accessed by client-side APIs. You can add your own httpOnly cookie attribute to your response.
- Java
server.response(cookie("loggedIn", "true", httpOnly()), status(302));- JSON
{
"request" :
{
"uri" : "/cookie"
},
"response" :
{
"cookies" :
{
"login" : {
"value" : "true",
"httpOnly" : "true"
}
}
}
}Max Age
@Since 0.11.1
The Max-Age attribute can be used to set the cookie's expiration as an interval of seconds in the future, relative to the time the browser received the cookie. You can add your own maxAge cookie attribute to your response.
- Java
server.response(cookie("loggedIn", "true", maxAge(1, TimeUnit.HOURS)), status(302))- JSON
{
"request" :
{
"uri" : "/cookie"
},
"response" :
{
"cookies" :
{
"login" : {
"value" : "true",
"maxAge": {
"duration": 1,
"unit": "hour"
}
}
}
}
}Same Site
@Since 1.5.0
The SameSite attribute can be set to control whether or not a cookie is sent with cross-site requests, providing some protection against cross-site request forgery attacks (CSRF).
Only "Strict", "Lax" and "None" can be set as sameSite value.
- Java
server.response(cookie("loggedIn", "true", sameSite("Lax")), status(302))- JSON
{
"request" :
{
"uri" : "/cookie"
},
"response" :
{
"cookies" :
{
"login" : {
"value" : "true",
"sameSite": "Lax"
}
}
}
}CORS
@Since 1.5.0
Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the resource originated. You can add your own cors to your response.
Default All CORS
You can add default all CORS to your response with cors operator without any arguments.
- Java API
server.response(cors());- JSON API
{
"response" :
{
"cors" : true
}
}CORS with allowOrigin/Access-Control-Allow-Origin
You can allow CORS with specific origin with cors operator with allowOrigin argument, which provides Access-Control-Allow-Origin header.
- Java API
server.response(cors(allowOrigin("https://www.github.com/")));- JSON API
{
"response" :
{
"cors" :
{
"allowOrigin" : "https://www.github.com/"
}
}
}If you allow any origin with * in origin.
- Java API
server.response(cors(origin("*")));- JSON
{
"response" :
{
"cors" :
{
"allowOrigin" : "*"
}
}
}In JSON API, you can also use Access-Control-Allow-Origin directly.
- JSON
{
"response" :
{
"cors" :
{
"Access-Control-Allow-Origin" : "https://www.github.com"
}
}
}CORS with allowMethods/Access-Control-Allow-Methods
You can allow CORS with specific methods with cors operator with allowMethods argument, which provides Access-Control-Allow-Methods header.
- Java API
server.response(cors(allowMethods("GET", "PUT")));
server.response(cors(allowMethods(HttpMethod.GET, HttpMethod.PUT)));- JSON
{
"response" :
{
"cors" :
{
"allowMethods" : ["GET", "PUT"],
}
}
}In JSON API, you can also use Access-Control-Allow-Methods directly.
- JSON
{
"response" :
{
"cors" :
{
"Access-Control-Allow-Methods" : ["GET", "PUT"]
}
}
}CORS with allowHeaders/Access-Control-Allow-Headers
You can allow CORS with specific headers with cors operator with allowHeaders argument, which provides Access-Control-Allow-Headers header.
- Java API
server.response(cors(allowHeaders("X-Header", "Y-Header")));- JSON
{
"response" :
{
"cors" :
{
"allowHeaders" : ["X-Header", "Y-Header"]
}
}
}In JSON API, you can also use Access-Control-Allow-Methods directly.
{
"response" :
{
"cors" :
{
"Access-Control-Allow-Headers" : ["X-Header", "Y-Header"]
}
}
}CORS with maxAge/Access-Control-Max-Age
You can allow CORS with specific max age with cors operator with maxAge argument, which provides Access-Control-Max-Age header.
- Java API
server.response(cors(maxAge(1728000, TimeUnit.SECONDS)));- JSON
{
"response" :
{
"cors" :
{
"maxAge": {
"duration": 1728000,
"unit": "second"
}
}
}
}In JSON API, you can also use Access-Control-Max-Age directly.
{
"response" :
{
"cors" :
{
"Access-Control-Max-Age" : {
"duration": 1728000,
"unit": "second"
}
}
}
}CORS with allowCredentials/Access-Control-Allow-Credentials
You can allow CORS with specific credentials with cors operator with allowCredentials argument, which provides Access-Control-Allow-Credentials header.
- Java API
server.response(cors(allowCredentials(true)));- JSON
{
"response" :
{
"cors" :
{
"allowCredentials" : true
}
}
}In JSON API, you can also use Access-Control-Allow-Credentials directly.
{
"response" :
{
"cors" :
{
"Access-Control-Allow-Credentials" : true
}
}
}CORS with exposeHeaders/Access-Control-Expose-Headers
You can allow CORS with specific expose headers with cors operator with exposeHeaders argument, which provides Access-Control-Expose-Headers header.
- Java API
server.response(cors(exposeHeaders("X-Header", "Y-Header")));- JSON
{
"response" :
{
"cors" :
{
"exposeHeaders" : ["X-Header", "Y-Header"]
}
}
}In JSON API, you can also use Access-Control-Expose-Headers directly.
{
"response" :
{
"cors" :
{
"Access-Control-Expose-Headers" : ["X-Header", "Y-Header"]
}
}
}Attachment
@Since 0.10.0
Attachment is often used in web development. You can setup an attachment in Moco as following. As you will see, you'd better set a filename for client to receive.
- Java API
server.get(by(uri("/"))).response(attachment("foo.txt", file("foo.response")));- JSON
{
"request": {
"uri": "/file_attachment"
},
"response": {
"attachment": {
"filename": "foo.txt",
"file": "foo.response"
}
}
}Latency
@Since 0.7
Sometimes, we need a latency to simulate slow server side operation.
@Since 0.10.1
It's easy to setup latency with time unit.
- Java API
server.response(latency(1, TimeUnit.SECONDS));- JSON
{
"request" :
{
"text" : "foo"
},
"response" :
{
"latency":
{
"duration": 1,
"unit": "second"
}
}
}The original API without time unit introduced in 0.7 has been deprecated.
Sequence
@Since 0.7
Sometimes, we want to simulate a real-world operation which change server side resource. For example:
- First time you request a resource and "foo" is returned
- We update this resource
- Again request the same URL, updated content, e.g. "bar" is expected.
We can do that by
server.request(by(uri("/seq"))).response(seq("foo", "bar", "blah"));The other response settings are able to be set as well.
server.request(by(uri("/seq"))).response(seq(status(302), status(302), status(200)));@Since 0.12.0
{
"request" : {
"uri" : "/seq"
},
"response": {
"seq": [
{
"text" : "foo"
},
{
"text" : "bar"
},
{
"text" : "blah"
}
]
}
}The other response settings are able to be set for json as well.
{
"request" : {
"uri" : "/seq"
},
"response": {
"seq": [
{
"status" : "302"
},
{
"status" : "302"
},
{
"status" : "200"
}
]
}
}Cycle
@Since 1.0.0
Cycle is similar to seq, but it will return response as cycle. An example is as following:
server.request(by(uri("/cycle"))).response(cycle("foo", "bar", "blah"));The response will returned as cycle:
- foo
- bar
- blah
- foo
- bar
- blah
- ...
The other response settings are able to be set as well.
server.request(by(uri("/cycle"))).response(cycle(status(302), status(302), status(200)));@Since 0.12.0
{
"request" : {
"uri" : "/cycle"
},
"response": {
"cycle": [
{
"text" : "foo"
},
{
"text" : "bar"
},
{
"text" : "blah"
}
]
}
}The other response settings are able to be set for json as well.
{
"request" : {
"uri" : "/cycle"
},
"response": {
"cycle": [
{
"status" : "302"
},
{
"status" : "302"
},
{
"status" : "200"
}
]
}
}JSON Response
If the response is JSON, we don't need to write JSON text with escape character in code.
@Since 0.10.2
You can give a POJO to Java API, it will be converted JSON text. Hint, this api will setup Content-Type header as well.
server.request(by(uri("/json"))).response(toJson(pojo));@Since 0.12.0toJson will be removed from 0.12.0, use json instead.
server.request(by(uri("/json"))).response(json(pojo));Note that this functionality is implemented in Jackson, please make sure your POJO is written in Jackson acceptable format.
@Since 0.9.2
For JSON API, just give json object directly
{
"request":
{
"uri": "/json"
},
"response":
{
"json":
{
"foo" : "bar"
}
}
}@Since 1.2.0
For API user, if you want to return create dynamic JSON based on the request, you can use lambda to do this.
- With request
server.request(by(uri("/json"))).response(json((request) -> new Pojo()));Mount
@Since 0.7
Moco allows us to mount a directory to uri.
- Java API
server.mount(dir, to("/uri"));- JSON
{
"mount" :
{
"dir" : "dir",
"uri" : "/uri"
}
}Glob is acceptable to filter specified files, e.g we can include by
- Java API
server.mount(dir, to("/uri"), include("*.txt"));- JSON
{
"mount" :
{
"dir" : "dir",
"uri" : "/uri",
"includes" :
[
"*.txt"
]
}
}or exclude by
- Java API
server.mount(dir, to("/uri"), exclude("*.txt"));- JSON
{
"mount" :
{
"dir" : "dir",
"uri" : "/uri",
"excludes" :
[
"*.txt"
]
}
}even compose them by
- Java API
server.mount(dir, to("/uri"), include("a.txt"), exclude("b.txt"), include("c.txt"));- JSON
{
"mount" :
{
"dir" : "dir",
"uri" : "/uri",
"includes" :
[
"a.txt",
"c.txt"
],
"excludes" :
[
"b.txt"
]
}
}@Since 0.10.1
You can also specify some response information like normal response, e.g.
- JSON
{
"mount" :
{
"dir" : "dir",
"uri" : "/uri",
"headers" : {
"Content-Type" : "text/plain"
}
}
}Template
Sometimes, we need to customize our response based on something, e.g. response should have same header with request.
The goal can be reached by template:
Request
You can get request information with req in template.
Version
@Since 0.8
With req.version, request version can be retrieved in template.
The following example will return response version as content.
- Java
server.request(by(uri("/template"))).response(template("${req.version}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.version}"
}
}
}Method
@Since 0.8
Request method is identified by req.method.
- Java
server.request(by(uri("/template"))).response(template("${req.method}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.method}"
}
}
}Content
@Since 0.8
All request content can be used in template with req.content
- Java
server.request(by(uri("/template"))).response(template("${req.content}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.content}"
}
}
}Header
@Since 0.8
Header is another important element in template and we can use req.headers for headers.
- Java
server.request(by(uri("/template"))).response(template("${req.headers['foo']}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.headers['foo']}"
}
}
}Query
@Since 0.8
req.queries helps us to extract request query.
- Java
server.request(by(uri("/template"))).response(template("${req.queries['foo']}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.queries['foo']}"
}
}
}Form
@Since 0.9.1
req.forms can extract form value from request.
- Java
server.request(by(uri("/template"))).response(template("${req.forms['foo']}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.forms['foo']}"
}
}
}Cookie
@Since 0.9.1
Cookie from request can extracted by req.cookies.
- Java
server.request(by(uri("/template"))).response(template("${req.cookies['foo']}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.cookies['foo']}"
}
}
}JSON
@Since 1.0.0
If your request is a JSON request, you can use req.json to visit your json object.
Note that make sure your request is a JSON request, otherwise an exception will be thrown.
- Java
server.request(by(uri("/template"))).response(template("${req.json.foo}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.json.foo}"
}
}
}XML
@Since 1.4.0
If your request is an XML request, you can use req.xml to visit your xml object.
Note that make sure your request is an XML request, otherwise an exception will be thrown.
- Java
server.request(by(uri("/template"))).response(template("${req.xml.foo}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.xml.foo}"
}
}
}Client
Address
@Since 1.4.0
req.client.address can be used in template to return client IP address.
server.request(by(uri("/template"))).response(template("${req.client.address}"));{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.client.address}"
}
}
}Port
@Since 1.5.0
req.client.port can be used in template to return client port.
server.request(by(uri("/template"))).response(template("${req.client.port}"));{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.client.port}"
}
}
}Path
@Since 1.4.0
req.path can work with path API to extract path parameter as template variable.
server.request(path(uri("/path/{foo}"))).response(template("${req.path.foo}"));{
"request": {
"uri": {
"path": "/path/{foo}"
}
},
"response": {
"text": {
"template": "${req.path.foo}"
}
}
}Custom Variable
@Since 0.9.1
You can provide your own variables in your template.
- Java
server.request(by(uri("/template"))).response(template("${foo}", "foo", "bar"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": {
"with" : "${foo}",
"vars" : {
"foo" : "bar"
}
}
}
}
}@Since 0.10.0
You can also use extractor to extract information from request.
- Java
server.request(by(uri("/template"))).response(template("${foo}", "foo", jsonPath("$.book[*].price")));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": {
"with" : "${foo}",
"vars" : {
"foo" : {
"json_path": "$.book[*].price"
}
}
}
}
}
}Other extractors, e.g. xpath also work here.
Template Function
now
@Since 1.0.0
Current time can retrieved by 'now' function and a date format string should be passed as argument.
- Java
server.request(by(uri("/template"))).response(template("${now('yyyy-MM-dd')}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${now(\"yyyy-MM-dd\")}"
}
}
}random
@Since 1.0.0
random will generate a random number. If you didn't pass any argument, the generated random will be between 0 and 1.
- Java
server.request(by(uri("/template"))).response(template("${random()}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${random()}"
}
}
}The first argument is random number range which means the generated number will be between 0 and range.
- Java
server.request(by(uri("/template"))).response(template("${random(100)}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${random(100)}"
}
}
}@Since 1.3.0
If you want to limit your random number in a range. You can give two number as a start and an end.
- Java
server.request(by(uri("/template"))).response(template("${random(99, 100)}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${random(99, 100)}"
}
}
}The last argument could be a number format.
- Java
server.request(by(uri("/template"))).response(template("${random(99, 100, '###.###')}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${random(99, 100, \"###.###\")}"
}
}
}You can also use number format directly without range. By default, range is from 0 to 1.
- Java
server.request(by(uri("/template"))).response(template("${random('###.###')}"));- JSON
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${random(\"###.###\")}"
}
}
}Redirect
@Since 0.10.2
Redirect can also be set as template.
- Java
server.request(by(uri("/redirectTemplate"))).redirectTo(template("${var}", "var", ""https://github.com"));- Json
{
"request" :
{
"uri" : "/redirect-with-template"
},
"redirectTo" : {
"template" : {
"with" : "${url}",
"vars" : {
"url" : "https://github.com"
}
}
}
}File Name Template
@Since 0.10.1
Template can also be used in file name, thus response can be different based on different request.
- Java
server.response(file(template("${req.headers['foo'].txt")));- JSON
[
{
"response": {
"file": {
"name": {
"template": "${req.headers['foo'].txt"}"
}
}
}
}
]Proxy
@Since 0.11.1
You can use template in proxy API, so that you can dynamically decide which URL you will forward the request to.
- Java
server.request(by(uri("/proxy"))).response(proxy(template("http://localhost:12306/${req.queries['foo']}")))- JSON
{
"request" : {
"uri" : "/proxy"
},
"response" : {
"proxy" : {
"url" : {
"template": "http://localhost:12306/${req.queries['foo']}"
}
}
}
}Template for Event Action
Template also can ben applied to event action. Check out Event for more details about event.
- Java
server.request(by(uri("/event"))).response("event").on(complete(post("http://localhost:12306/target"), template("${target}", of("target", var("target"))))));- JSON
{
"request": {
"uri": "/event"
},
"response": {
"text": "event"
},
"on": {
"complete": {
"post": {
"url": "http://localhost:12306/target",
"content": {
"template": {
"with": "${target}",
"vars": {
"target" : "target"
}
}
}
}
}
}
}Record and Replay
@Since 1.1.0
More powerful dynamic features are required even if you can implement some with a template. For instance, you may want to change one URL to return a different response. Record and replay will help.
In the following, /record will be used to record request and /replay will return the recorded request content. You can also configure record and replay for more capabilities.
In this case, group will be used to distinguish different record sources.
server.request(by(uri("/record"))).response(record(group("foo")));
server.request(by(uri("/replay"))).response(replay(group("foo")));Group
group help you distinguish different record source, which means you can have same configuration in different group. If no group is provided, default group will be applied.
- Java
server.request(by(uri("/record"))).response(record(group("foo")));
server.request(by(uri("/replay"))).response(replay(group("foo")));- JSON
The default parameter for record and replay is group.
[
{
"request" : {
"uri" : "/record"
},
"response" : {
"record" : "foo"
}
},
{
"request" : {
"uri" : "/replay"
},
"response" : {
"record" : "foo"
}
}
]You can also specify group name explicitly.
[
{
"request" : {
"uri" : "/record"
},
"response" : {
"record" : {
"group": "foo"
}
}
},
{
"request" : {
"uri" : "/replay"
},
"response" : {
"record" : {
"group": "foo"
}
}
}
]Identifier
In the same group, you can use identifier to distinguish different requst. You can extract identifier from request with template syntax.
- Java
server.request(by(uri("/record"))).response(record(identifier("${req.queries['type']}")));
server.request(by(uri("/replay"))).response(replay(identifier("${req.queries['type']}")));- JSON
[
{
"request" : {
"uri" : "/record"
},
"response" : {
"record" : {
"identifier": {
"template": "${req.queries['type']}"
}
}
}
},
{
"request" : {
"uri" : "/replay"
},
"response" : {
"record" : {
"identifier": {
"template": "${req.queries['type']}"
}
}
}
}
]In the above case, if you record your request with
- URI
/record?type=fooand content isfoo - URI
/record?type=barand content isbar,
When you access URI /replay?type=foo, foo will be returned and access URI /replay?type=bar, bar will be returned.
Modifier
The recorded content will be returned by default. But sometimes you hope return different content.
In the following case, with modifier, type in request parameter will be returned as replay content. As you expect, modifier only apply in replay. Template syntax will apply here.
- Java
server.request(by(uri("/record"))).response(record(group("foo")));
server.request(by(uri("/replay"))).response(replay(group("foo"),
modifier("${req.queries['type']}")));- JSON
[
{
"request" : {
"uri" : "/record"
},
"response" : {
"record" : {
"group": "foo",
}
}
},
{
"request" : {
"uri" : "/replay"
},
"response" : {
"record" : {
"group": "foo",
"modifier": "${req.queries['type']}"
}
}
}
]By default, only response content will be set with modifier. If you want more, you can setup the response with other response configuration.
- Java
server.request(by(uri("/record"))).response(record(group("foo")));
server.request(by(uri("/replay"))).response(replay(group("foo"),
modifier(template("${req.content}"),
header("X-REPLAY", template("${req.queries['type']}"))
)
));- JSON
[
{
"request" : {
"uri" : "/record"
},
"response" : {
"record" : {
"group": "foo",
}
}
},
{
"request" : {
"uri" : "/replay"
},
"response" : {
"record" : {
"group": "foo",
"modifier": {
"text": {
"template": "${req.content}"
},
"header": {
"X-REPLAY": {
"template": "${req.queries['type']}"
}
}
}
}
}
}
]Tape
If you want to reuse recorded request after restart, tape will help you persist the recorded request just like a tape.
- Java
server.request(by(uri("/record"))).response(record(group("foo"), tape("/path/to/tape")));
server.request(by(uri("/foo-replay"))).response(replay(record(group("foo"),
tape("/path/to/tape")));- JSON
[
{
"request" : {
"uri" : "/record"
},
"response" : {
"record" : {
"group": "foo",
"tape": "/path/to/tape"
}
}
},
{
"request" : {
"uri" : "/replay"
},
"response" : {
"record" : {
"group": "foo",
"tape": "/path/to/tape"
}
}
}
]Event
You may need to request another site when you receive a request, e.g. OAuth. Event could be your helper at that time.
Complete
@Since 0.9
Complete event will be fired after your request has been handled completely.
Get Request
You can get from an URL.
- Java
server.request(by(uri("/event"))).response("event").on(complete(get("http://another_site")));- JSON
{
"request": {
"uri" : "/event"
},
"response": {
"text": "event"
},
"on": {
"complete": {
"get" : {
"url" : "http://another_site"
}
}
}
}@Since 1.0.0
And also get with headers.
- Java
server.request(by(uri("/event"))).response("event").on(complete(get("http://another_site", header("foo", "bar"))));- JSON
{
"request": {
"uri" : "/event"
},
"response": {
"text": "event"
},
"on": {
"complete": {
"get" : {
"url" : "http://another_site",
"headers": {
"foo": "bar"
}
}
}
}
}Post Request
You can post some content as well.
- Java
server.request(by(uri("/event"))).response("event").on(complete(post("http://another_site", "content")));- JSON
{
"request": {
"uri" : "/event"
},
"response": {
"text": "event"
},
"on": {
"complete": {
"post" : {
"url" : "http://another_site",
"content": "content"
}
}
}
}@Since 0.12.0
If your post content is JSON, you can use json in your configuration directly.
{
"request": {
"uri" : "/event"
},
"response": {
"text": "event"
},
"on": {
"complete": {
"post" : {
"url" : "http://another_site",
"json": {
"foo" : "bar"
}
}
}
}
}@Since 1.0.0
And also post with headers.
- Java
server.request(by(uri("/event"))).response("event").on(complete(post("http://another_site", "content", header("foo", "bar"))));- JSON
{
"request": {
"uri" : "/event"
},
"response": {
"text": "event"
},
"on": {
"complete": {
"post" : {
"url" : "http://another_site",
"content": "content",
"headers": {
"foo": "bar"
}
}
}
}
}Let me know if you need more methods.
Asynchronous
@Since 0.9
Synchronized request is used by default, which means response won't be returned to client until event handler is finished.
If it is not your expected behavior, you can changed with async API which will fire event asynchronously.
- Java
server.request(by(uri("/event"))).response("event").on(complete(async(post("http://another_site", "content"))));- JSON
{
"request": {
"uri" : "/event"
},
"response": {
"text": "event"
},
"on": {
"complete": {
"async" : "true",
"post" : {
"url" : "http://another_site",
"content": "content"
}
}
}
}You can specify latency for this asynchronous request as well to wait a while.
- Java
server.request(by(uri("/event"))).response("event").on(complete(async(post("http://another_site", "content"), latency(1000))));- JSON
{
"request": {
"uri" : "/event"
},
"response": {
"text": "event"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"post" : {
"url" : "http://another_site",
"content": "content"
}
}
}
}Verify
@Since 0.9
Someone may want to verify what kind of request has been sent to server in testing framework.
You can verify request like this:
RequestHit hit = requestHit();
final HttpServer server = httpServer(12306, hit);
server.get(by(uri("/foo"))).response("bar");
running(server, () -> {
assertThat(helper.get(remoteUrl("/foo")), is("bar"));
});
hit.verify(by(uri("/foo"))), times(1));You can also verify unexpected request like this:
hit.verify(unexpected(), never());Many verification can be used:
- never: none of this kind of request has been sent.
- once: only once this kind of request has been sent.
- time: how many times this kind of request has been sent.
- atLeast: at least how many time this kind of request has been sent.
- atMost: at most how many time this kind of request has been sent.
- between: the times this kind of request has been sent should be between min and max times.
Miscellaneous
Port
@Since 0.9
If you specify a port for your stub server, it means the port must be available when you start server. This is not case sometimes.
Moco provides you another way to start your server: specify no port, and it will look up an available port. The port can be got by port() method. The example is as follow:
final HttpServer server = httpServer();
server.response("foo");
running(server, () -> {
Content content = Request.Get("http://localhost:" + server.port()).execute().returnContent();
assertThat(content.asString(), is("foo"));
});The port will be returned only when server is started, otherwise the exception will be thrown.
For standalone server, if you need this behaviour, simply don't give port argument.
java -jar moco-runner-<version>-standalone.jar http -c foo.jsonThe port information will shown on screen.
Log
@Since 0.9.1
If you want to know more about how your Moco server running, log will be your helper.
final HttpServer server = httpServer(log());The Moco server will log all your requests and responses in your console.
It you want to keep log, you can use log interface as following:
final HttpServer server = httpServer(log("path/to/log.log"));@Since 0.10.1
Log content may contain some non UTF-8 character, charset could be specified in log API:
final HttpServer server = httpServer(log("path/to/log.log", Charset.forName("GBK")));The log will be saved in your log file.
Log with verifier
Log will help you for some legacy system to know what detailed request/response looks like. You also need to do some verification work. Here is the case.
RequestHit hit = requestHit();
final HttpServer server = httpServer(port(), hit, log());File: moco-doc/cmd.md
Standalone Command Line
Moco can be used as standalone to run with configuration and you can download standalone directly:
Standalone Moco Runner
First of all, a JSON configuration file needs to be provided to start Moco.
[
{
"response" :
{
"text" : "foo"
}
}
](foo.json)
Table of Contents
Server Type
A server type is a must for Moco, you need specify your server type as first argument. Different server type will support different arguments.
HTTP Server
You can start a HTTP server by the following command:
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.jsonHTTPS Server
A HTTPS server can be started by the following command:
java -jar moco-runner-<version>-standalone.jar https -p 12306 -c foo.json --https /path/to/cert.jks --cert mocohttps --keystore mocohttpsConfiguration Files
One Configuration File
You can specify your configuration with -c.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.jsonMany Configuration Files
@Since 0.12.0
If you have many configuration files, you can use glob matcher as -c argument.
But you need to make sure the configuration that don't conflict.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c "*.json"Note: the quotation mark is required otherwise *.json will be parsed by your shell.
Port
Specific Port
You can specify server port with -p
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.jsonWithout Port
If you don't need any specified port, you run run Moco without port.
java -jar moco-runner-<version>-standalone.jar http -c foo.jsonAn available port will be picked up by Moco and you can see the port in console.
20 Oct 2015 22:10:18 [main] INFO Server is started at 58593
20 Oct 2015 22:10:18 [main] INFO Shutdown port is 58594Quiet Mode
If you don't want see too many logs, you could make the server quiet with -q option.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json -qMax Content Length
Max content length for a single request is set by default, but it is not enough for some case. You can set your own content length with content-length arguments.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json --content-length 2097152Version
You can query Moco version by the following command:
java -jar moco-runner-<version>-standalone.jar versionGlobal Settings
You can run Moco instance with global settings.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -g settings.jsonEnvironment
Environment is a good feature, which allows you start your server with different environment from CLI.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -g env.json -e remoteShutdown
Moco instance can ben shutdown by shutdown command and its shutdown port. A shutdown port can be specified when start Moco instance.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json -s 9527Or leave Moco to choose by default, the shutdown port will be shown on console.
Then you can use the shutdown port to shutdown the running Moco instance.
java -jar moco-runner-<version>-standalone.jar shutdown -s 9527File: moco-doc/configurations.md
moved to APIs
File: moco-doc/extending.md
Extending Moco
You can extend Moco, if you need more features than Moco already provides.
The basic idea is very simple: RequestMatcher and ResponseHandler. If your request matches any matcher, the corresponding response handler will be invoked to return response.
Request Matcher
@Since 1.3.0
If you want to implement your own matcher, you can write with conditional API which is supported in Java code.
server.request(conditional(request -> request.getContent().toString().equals("foo"))).response("foo");Response Handler
If you want to write response based on the request, simply write a ResponseHandler.
public interface ResponseHandler extends ConfigApplier<ResponseHandler> {
void writeToResponse(SessionContext context);
}You just want to rewrite your own HTTP content in most cases, you can extend AbstractHttpContentResponseHandler.
import com.github.dreamhead.moco.util.MediaType;
public class AbstractContentResponseHandler extends AbstractHttpContentResponseHandler {
protected MessageContent responseContent(HttpRequest httpRequest) {
return MessageContent.content("hello Moco");
}
protected abstract MediaType getContentType(HttpRequest request) {
return MediaType.PLAIN_TEXT_UTF_8;
}
}Since 2.0.0 MediaType is com.github.dreamhead.moco.util.MediaType. Before 2.0.0 it was
Guava's com.google.common.net.MediaType.
File: moco-doc/global-settings.md
Global Settings
We could put all configurations in one single configuration files. But if we want stub many services in a single Moco instance, the configurations file would be huge.
In this case, we can use settings file to separate our configurations for different into different configuration files.
It's example time. We have two services to stub:
[
{
"request" : {
"uri" : "/foo"
},
"response" : {
"text" : "foo"
}
}
]and
[
{
"request" : {
"uri" : "/bar"
},
"response" : {
"text" : "bar"
}
}
]Now, we can write a setting file to combine these two configurations:
[
{
"include" : "foo.json"
},
{
"include" : "bar.json"
}
]It's time start server with this setting:
java -jar moco-runner-<version>-standalone.jar http -p 12306 -g settings.jsonFeel free to open your browser to try.
Table of Contents
Configuration
In fact, there are some configurations for settings to simplify your configuration.
Context
@Since 0.8
We can put all responses for one service in a specified context:
[
{
"context": "/foo",
"include": "foo.json"
},
{
"context": "/bar",
"include": "bar.json"
}
]Now all configurations in foo.json must be accessed by /foo context. In this case, when you access http://localhost:12306/foo/foo, you will get "foo".
On the other hand, bar.json will in /bar context which means http://localhost:12306/bar/bar will return "bar".
File Root
@Since 0.8
If you have many file APIs in your configuration, file root setting will help you to shorten configurations.
As the name suggests, file root setting will play as the file root for configuration. So all your file APIs could be used as the relative path.
[
{
"file_root": "fileroot",
"include": "fileroot.json"
}
]Now, both include setting and file APIs use relative path. In this case, fileroot/fileroot.json will be used as included file.
[
{
"request" : {
"uri" : "/fileroot"
},
"response" : {
"file" : "foo.response"
}
}
](fileroot/fileroot.json)
When the request is launched, src/test/resources/foo.response will be returned.
Environment
@Since 0.8
For some different cases, you have different configuration. For example, your code may access proxy for remote server and mock server for local testing.
Now, environment will help you to configure all related configurations in one settings.
[
{
"env" : "remote",
"include": "foo.json"
},
{
"env" : "local",
"include": "bar.json"
}
](env.json)
You can start your server with different environment from CLI.
java -jar moco-runner-<version>-standalone.jar http -p 12306 -g env.json -e remoteNow, when you access your server, all configurations with "remote" environment rocks!
In this case, http://localhost:12306/foo will give you "foo", but http://localhost:12306/bar will return nothing.
Request
@Since 0.10.1
You may need a global request matcher in some cases, for example, token for some REST APIs, which actually is request parameter. Global request will help you.
[
{
"request" : {
"headers" : {
"foo" : "bar"
}
},
"include": "blah.json"
}
]In this case, you won't receive response until your request has "foo", "bar" header.
Response
@Since 0.9.1
In some cases, you may want to setup a global response for all response, for example, HTTP version, or HTTP header, so you don't have to setup it for every single response.
[
{
"response" : {
"headers" : {
"foo" : "bar"
}
},
"include": "blah.json"
}
]When you issue any request to server, it will return response with "foo", "bar" header.
Glob Inclusion
@Since 0.12.0
You may need include many files, glob can help you to do this.
[
{
"include" : "*.json"
}
]File: moco-doc/junit5.md
JUnit 5 Integration
@Since will be at next release
Moco makes use of Extension in JUnit 5 to simplify JUnit integration.
@MocoExtension is required to use Moco in JUnit 5. Every test class use Moco should be annotated with @ExtendWith(MocoExtension.class).
@ExtendWith(MocoExtension.class)
class WithMocoTest {
...
}With @MocoExtesion, Moco server will be started before test and stopped after test.
HTTP Server
@MocoHttpServer could be used to start a HTTP server. Configuration file and port are required.
@ExtendWith(MocoExtension.class)
@MocoHttpServer(filepath = "foo/foo.json", port=12306)
public class MocoHttpTest {
@Test
public void should_return_expected_message() throws IOException {
Content content = Request.Get("http://localhost:12306").execute().returnContent();
assertThat(content.asString(), is("foo"));
}
}filepath is provided here, you can also use classpath to retrieve configuration file from classpath.
@ExtendWith(MocoExtension.class)
@MocoHttpServer(classpath = "foo.json", port=12306)
public class MocoHttpTest {
...
}HTTPS Server
Besides @MocoHttpServer, you should also use @MocoCertificate to start Moco as a HTTPS server.
@ExtendWith(MocoExtension.class)
@MocoHttpServer(filepath = "foo/foo.json", port=12306)
@MocoCertificate(filepath = "certificate/cert.jks", keyStorePassword = "mocohttps", certPassword = "mocohttps")
public class MocoHttpTest {
@Test
public void should_return_expected_message() throws IOException {
Content content = Request.Get("http://localhost:12306").execute().returnContent();
assertThat(content.asString(), is("foo"));
}
}Certificate file, keyStorePassword and certPassword can be configured with @MocoCertificate.
You can also configure certificate file with classpath.
@ExtendWith(MocoExtension.class)
@MocoHttpServer(classpath = "foo.json", port=12306)
@MocoCertificate(classpath = "certificate/cert.jks", keyStorePassword = "mocohttps", certPassword = "mocohttps")
public class MocoHttpTest {
@Test
public void should_return_expected_message() throws IOException {
...
}
}File: moco-doc/plan.md
Functionality
APIs
Matcher
- Marshal support
Handler
- Template function
More
- Client
Document
- Cookbook
Tools Support
- IntelliJ IDEA plugin
- Eclipse plugin
- Runtime configuration change
Polyglot
- Groovy
- Scala
- Clojure
- Node.js
File: moco-doc/ReleaseNotes.md
Release 2.0.0(unreleased)
Breaking changes
- A BREAK CHANGE: remove socket api
- A BREAK CHANGE: remove JUnit 4 support (
moco-junitmodule). Migrate tomoco-junit5. - Guava is no longer a dependency. Moco's public API no longer exposes any Guava type, which means
several signatures changed. Recompilation is required even where the source still compiles,
because the erased return types changed.ImmutableMapandImmutableListin the API are nowjava.util.Mapandjava.util.List. This
affectsHttpMessage.getHeaders,HttpRequest.getQueries,DefaultHttpRequest.getForms,getCookies,Moco.template(..., variables),Jsons.toObjects,Globs.glob,Configs.configItems,SettingFetcher.getSettingsand theRunnerSetting/parser types.
Code that stored a result in anImmutableMap/ImmutableListvariable, or that implementsHttpMessage,HttpRequest,ContentorSettingFetcher, must be updated.MediaTypemoved fromcom.google.common.net.MediaTypetocom.github.dreamhead.moco.util.MediaType. It keepscreate,parse,type,subtype,charset,withCharsetand the constants Moco used, and renders identically on the wire.charset()now returnsjava.util.Optional, so a trailing.toJavaUtil()should be dropped.FileContentType.DEFAULT_CONTENT_TYPE_WITH_CHARSETchanges type accordingly.CookiesRequestExtractorandFormsRequestExtractornow extendHttpRequestExtractor<Map<String, String>>.
MocoMonitorno longer carries Guava's@Subscribeon its methods. Moco has had noEventBus
since 2013, so this changes nothing inside Moco. It does matter if you registered aMocoMonitoron your own GuavaEventBus, because Guava resolves@Subscribethrough
interfaces: those methods will now silently never be registered, with no compile error.
Annotate your own overrides with@Subscribeinstead.
APIs
- HTTP 2 Support
Implementation Details
- Removed the Guava dependency from all modules. The standalone jar shrinks by roughly 210 KB and
the uber jar by about 3 MB. org.jspecify:jspecifyis now declared explicitly ascompileOnly; it previously arrived only
as a transitive dependency of Guava.- A
checkNoGuavaverification task now fails the build if Guava is reimported.
Release 1.6.1(2-May-2026)
APIs
- SSE event delay semantics changed: delay affects current event self.
Implementation Details
- SSE support in proxy failover
Release 1.6.0(20-Apr-2026)
APIs
sseAPI for Server-Sent Events streaming supporteventfor named SSE eventdatafor anonymous SSE eventsse(...).delay(ms)for global delay between all eventssse(...).delay(duration, TimeUnit)for global delay with time unit- Event modifiers:
id,retry,delay event.delay(duration, TimeUnit)for event delay with time unit
- SSE events from file
- SSE proxy: proxy forwards SSE streams from remote server
- SSE JSON API
- Array format for inline events
- Object format with
delayandevents - Object format with
delayas{duration, unit}for time unit - File format with
delay
Implementation Details
- Replace logback with tinylog
- Optimize ProGuard configuration to exclude unused resources
Release 1.5.0(1-Sep-2023)
APIs
req.client.portis supported in templatesameSitecookie attribute is supportedcorsin response is supported for CORSallowOrigin(Access-Control-Allow-Credentials)allowCredentials(Access-Control-Allow-Credentials)exposeHeaders(Access-Control-Expose-Headers)maxAge(Access-Control-Max-Age)allowMethods(Access-Control-Allow-Methods)allowHeaders(Access-Control-Allow-Headers)
Release 1.4.0 (1-Dec-2022)
APIs
pathis introduced for urireq.xmlis supported in templatereq.client.addressis supported in templatereq.pathis supported in template
CLI
- add
-qfor quiet mode in standalone server
Implementation Details
- enhance log with query parameters
- event action log is supported
Release 1.3.0 (1-Jan-2022)
APIs
conditionalJava API allows you to write your own matcherrandomtemplate function supports start and end as rangestructAPI for JSON and XML which matches only struct no matter what content is.
Implementation Details
- restart with configuration when new configuration is illegal. #304
- enhance log with query parameters
xmlreturns ContentResource which is A BREAK CHANGE.
Release 1.2.0 (1-Apr-2021)
Websocket
connectedAPI return content when client connects to websocket serverrequestmatch request contentresponsereturn response contentjoinadd client to specific groupbroadcastbroadcast message to all subscriber.groupbroadcast message to specific group
APIs
transformJava API allows you transform your content.binaryJava API return binary message.jsonJava API return pojo dynamically.text,binary,json,varwith function, Java API allows you write your own response content.
Implementation Details
- Return application/json header for
jsonAPI with text and resource.
Release 1.1.0 (1-Mar-2020)
- Upgrade minimal Java version to Java 8.
APIs
recordandreplaysupport dynamic stub server.groupidentifiermodifiertape
Release 1.0.0 (1-Sep-2019)
APIs
cycleAPI returns response as a cycle.nowas template function is supported in templaterandomas template function is supported in templatereq.jsonis supported in templatefailoverandplaybackstatus API is supported.headerreturns HttpHeader instead of ResponseHandler.getandpostfor event action supports HTTP header.
Implementation Details
- Support multiple HTTP headers with same name
- Support proxy PATCH method
- Dump HTTP message even if Content Length header is missing
Release 0.12.0 (24-Dec-2017)
- Upgrade minimal Java version to Java 7
APIs
jsonAPI returns resource, so it can be used for both request and response.andAPI for Response Handler
JSON APIs
seqis supported finallyjsonin Post event
Global Settings
- Glob syntax is supported for include files in global setting
CLI
- Glob syntax is supported for CLI '-c' argument
Release 0.11.1 (12-Jun-2017)
APIs
- Template for Event action
- Remove publish API with Optional, e.g. file and pathResource
- Proxy with content resource as url
- Add cookie attribute API
JUnit
- HTTPS JUnit runner for POJO, JSON and from path resource
- Http JSON runner with path resource
- Socket JSON runner with path resource
Implementation Details
- Fix: get Moco standalone version from manifest
- Fix: merge server with port
- Fix: set charset from http request
- Fix: parse json with charset
- Fix: fail to load configuration with multiple modification
- Fix: allowed empty query
- Fix: make request hit and moco monitor thread safe
- Fix: parse form with charset
Release 0.11.0 (20-May-2016)
Usage
- JUnit Integration
APIs
REST API
- get
- post
- put
- delete
- head
- patch
- sub-resource
- any id
Implementation Details
- Fix: response handler case issue
- Write JSON content to log
- Fix: empty content for jsonpath
- Fix: empty content for xpath
Release 0.10.2 (1-Sep-2015)
APIs
- toJson will convert POJO to JSON text in response handler
- json with pojo will convert POJO to JSON text in request matcher
- socketServer without port
- socketServer with monitor, e.g. logging and verification
- Template as redirect target
Implementation Details
- Enhance log dumper with MediaType implementation
- Enhance socket server implementation with byte buf, rather than string
- Fix charset issue in toJson
- Enhance file content type with MediaType implementation
- Same name query has been supported
Release 0.10.1 (1-May-2015)
APIs
- Use httpServer as API, and httpserver has been deprecated, will be removed at next release.
- File and path resource with charset
- Latency with time unit, and latency without time unit has been deprecated, will be removed at next release
- Log file with charset
- Template as filename for file and path resource
JSON APIs
- Mount with response setting
Global Settings
- Request
Implementation Details
- File and path resource read as byte array to fix file encoding issue
- JSON path and XPath in template will return many objects.
- Changed template extractor name to singular in JSON API, e.g. json_paths to json_path, xpaths to xpath.
Release 0.10.0 (1-Dec-2014)
Usage
- Socket support
- Version query
- Start arguments have been changed from start to HTTP(HTTPS, Socket) etc.
APIs
- Attachment
Runner API
- server by JSON configuration
Template
- Template extractor support
Shell
- Added version argument
Potential Breaks
- JSON Path, because new version JSON Path implementation has been introduced.
Release 0.9.2 (1-Jul-2014)
Usage
- (Beta) HTTPS support
APIs
- Http Protocol Version in Java API.
- More operators:
** startsWith
** endsWith
** contain
** exist - between for verifier
- Added multiple monitors for server creation.
JSON APIs
- json text shortcut
Template
- change template variables from Object to String.
(API users have to decide how to display their vars). - JSON request/response shortcut in JSON configuration.
Release 0.9.1 (1-Feb-2014)
Usage
- Runner API
- (Beta) gmoco, moco groovy shell
APIs
- log to record request
- once for verifier
- batch proxy
- proxy playback
Template
- cookie
- form
- template variables
Global Settings
- Response
Implementation Details
- Fixed "uri" in failover to remove query parameter.
- Used Proguard to minimize standalone jar
Fixes
- Keep connect alive if request wants to keep alive
- NPE for validating form/content in request hit
- NPE for XPath matcher
- Wait server to shutdown completely
Release 0.9 (1-Nov-2013)
APIs
- Request Hit as verifier
- Event API
** Event: Complete
** Event Action: Get/Post URL - Turned off log for API by default
- seq API with response handler
- Added shortcut for PUT and DELETE method
Implementation Details
- Server can select available port(start/shutdown) without providing no port.
- Changed "statusCode" in failover to "status". Please changed your failover file if your tests failed.
Release 0.8.1 (1-August-2013)
- Upgrade to Netty 4
- Fixed proxy bug
Release 0.8 (21-July-2013)
Usage
- global setting file
APIs
Request
- JSONPath
Response
Integration
- proxy
- with
- (Beta) template
- removed url (using proxy instead)
- removed content (using with instead)
Misc Response
- removed cache
Template
- version
- method
- content
- header
- query
Global Settings
- context
- file root
- environment
Release 0.7 (1-May-2013)
the first public release of Moco
Usage
- API
- standalone server
- shell
- maven plugin (external)
- gradle plugin (external)
APIs
Request
HTTP
- content(text, file)
- URI
- query Parameter
- HTTP method
- version
- header
- redirect
Operator
- eq
- match
Web
- cookie
- form
Integration
- XML
- XPath
- JSON
Response
HTTP
- content(text, file)
- version
- status code
- header
Integration
- url
Misc Response
- latency
- sequence(API only)
- cache
Misc
- mount
File: moco-doc/rest-apis.md
REST APIs
Restful service is a defacto service design style. Moco provides REST APIs which facilitate restful service stub implementation.
Table of Contents
Basic Usage
You will create a rest server with restServer as follows:
RestServer server = restServer(port, log());
ResourceObject resource = new ResourceObject();
resource.code = 1;
resource.message = "hello";
server.resource("targets",
get("1").response(toJson(resource))
);In this case, you create a resource named targets. If you access this resource with /targets/1 (1 is the ID for this resource) in GET method, a resource object will be returned.
Note: Restful service is also an HTTP service by default, so HTTP(s) APIs also available to REST server. As you have seen here, toJson is a response handler from Moco.
REST APIs is also available in JSON APIs. The same resource could be created in JSON APIs as follows:
[
{
"resource": {
"name": "targets",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 1,
"message": "foo"
}
}
}
]
}
}
]Composite REST Settings
Many REST settings could be added with resource API as follows:
server.resource("targets",
get("1").response(toJson(resource1)),
get("2").response(toJson(resource2)),
post().response(status(201), header("Location", "/targets/123"))
);Request and Response
You can also setup request and response for every single REST setting as you've done with regular Moco APIs.
server.resource("targets",
get("1").request(eq(header("Content-Type"), "application/json")).response(toJson(resource))
);Visit HTTP(s) APIs for more request and response settings.
ID Matcher
In REST API design, ID is used to identify a specified resource. You can specify a ID with a string.
server.resource("targets",
get("1").response(toJson(resource))
);Or you can match your single resource with ID matcher. Currently, anyId is provided in Moco REST APIs which means the following setting will cover both /targets/1 and /targets/2, even /targets/anything.
server.resource("targets",
get(anyId()).response(toJson(resource))
);* can play as any id in JSON API.
"resource": {
"name": "targets",
"get": [
{
"id": "*",
"response": {
"json": {
"code": 1,
"message": "foo"
}
}
}
]
}Methods
GET
Get with ID
@Since 0.11.0
In REST API design, GET method is used to query. If an ID is specified, it is used to query a single resource. The following setting could be visited with /targets/1 in GET method.
- Java
server.resource("targets",
get("1").response(toJson(resource))
);- JSON
"resource": {
"name": "targets",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 1,
"message": "foo"
}
}
}
]
}Get All
@Since 0.11.0
If no id specified, all related resource will be returned. The following setting could be visited with /targets in GET method.
- Java
server.resource("targets",
get().response(toJson(Arrays.asList(resource)))
);- JSON
"resource": {
"name": "targets",
"get": [
{
"response": {
"json": {
"code": 1,
"message": "foo"
}
}
}
]
}POST
@Since 0.11.0
POST method is used to create new resource. The following setting could be visited with /targets in POST method.
- Java
server.resource("targets",
post().response(status(201), header("Location", "/targets/123"))
);- JSON
"resource": {
"name": "targets",
"post": [
{
"response": {
"status": 201,
"headers" : {
"Location": "/targets/123"
}
}
}
]
}PUT
@Since 0.11.0
PUT method is used to update a specified resource. The following setting could be visited with /targets/1 in PUT method.
- Java
server.resource("targets",
put("1").response(status(200))
);- JSON
"resource": {
"name": "targets",
"put": [
{
"id": 1,
"response": {
"status": 200,
}
}
]
}DELETE
@Since 0.11.0
DELETE method is used to delete a specified resource. The following setting could be visited with /targets/1 in DELETE method.
- Java
server.resource("targets",
delete("1").response(status(200))
);- JSON
"resource": {
"name": "targets",
"delete": [
{
"id": 1,
"response": {
"status": 200,
}
}
]
}HEAD
HEAD with ID
@Since 0.11.0
HEAD method is used to query resource metadata. If an ID is specified, it is used to query a single resource. The following setting could be visited with /targets/1 in HEAD method.
- Java
server.resource("targets",
head("1").response(header("ETag", "Moco"))
);- JSON
"resource": {
"name": "targets",
"get": [
{
"id": "1",
"response": {
"headers": {
"ETag": "Moco"
}
}
}
]
}HEAD All
@Since 0.11.0
If no id specified, all related resource metadata will be returned. The following setting could be visited with /targets in HEAD method.
- Java
server.resource("targets",
head().response(header("ETag", "Moco"))
);- JSON
"resource": {
"name": "targets",
"get": [
{
"response": {
"headers": {
"ETag": "Moco"
}
}
}
]
}PATCH
@Since 0.11.0
PATCH method is used to update a specified resource. The following setting could be visited with /targets/1 in PATCH method.
- Java
server.resource("targets",
patch("1").response(status(200))
);- JSON
"resource": {
"name": "targets",
"patch": [
{
"id": 1,
"response": {
"status": 200,
}
}
]
}Sub-resource
@Since 0.11.0
Sub-resource is used to build relationship for resource. The following setting could be visited with /targets/1/subs/1 in GET method.
- Java
server.resource("targets",
id("1").name("subs").settings(
get("1").response(toJson(resource)),
)
);- JSON
"resource": {
"name": "targets",
"resource": [
{
"id": "1",
"name": "subs",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 3,
"message": "sub"
}
}
}
]
}
]
}The REST settings in sub-resource are the same as regular REST API.
File: moco-doc/sse-apis.md
SSE APIs
@Since 1.6.0
Server-Sent Events (SSE) streaming support for mocking real-time event streams, including LLM-style token output.
Basic Usage
import static com.github.dreamhead.moco.Moco.by;
import static com.github.dreamhead.moco.Moco.uri;
import static com.github.dreamhead.moco.MocoSse.event;
import static com.github.dreamhead.moco.MocoSse.sse;
HttpServer server = httpServer(12306);
server.request(by(uri("/events")))
.response(sse(event("message", "Hello")));API Reference
MocoSse Factory Class
All SSE APIs are in MocoSse:
import static com.github.dreamhead.moco.MocoSse.event;
import static com.github.dreamhead.moco.MocoSse.data;
import static com.github.dreamhead.moco.MocoSse.sse;sse(SseEvent first, SseEvent... rest)
Creates an SSE streaming response handler with one or more events.
- Java API
server.request(by(uri("/events")))
.response(sse(
event("message", "Hello"),
event("message", " World")
));- JSON
{
"request" : {
"uri" : "/events"
},
"response" : {
"sse" : [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": " World" }
]
}
}sse(Resource resource)
Creates an SSE streaming response from a resource file.
- Java API
server.request(by(uri("/events")))
.response(sse(file("events.txt")));- JSON
{
"request" : {
"uri" : "/events"
},
"response" : {
"sse" : {
"file" : "events.txt"
}
}
}sse(...).delay(long)
Sets a default delay for all events without their own delay. Event-level delay takes precedence.
- Java API
sse(event("message", "Hello"), event("message", " World")).delay(50) // milliseconds
sse(event("message", "Hello"), event("message", " World")).delay(1, TimeUnit.SECONDS)- JSON
{
"response" : {
"sse" : {
"delay" : 50,
"events" : [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": " World" }
]
}
}
}With time unit:
{
"response" : {
"sse" : {
"delay" : { "duration": 1, "unit": "SECOND" },
"events" : [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": " World" }
]
}
}
}Supported units: NANOSECOND, MICROSECOND, MILLISECOND, SECOND, MINUTE, HOUR, DAY.
event(String name, String data, String... rest)
Creates a named SSE event.
- Java API
event("message", "Hello World")data(String data, String... rest)
Creates an anonymous SSE event (no event name).
- Java API
data("Hello World")Event Modifiers
Chain modifiers on events:
- Java API
event("message", "Hello").id("1") // Set event ID
event("message", "Hello").retry(3000) // Set reconnection time (ms)
event("message", "Hello").delay(50) // Set send delay (ms), must be > 0
event("message", "Hello").delay(1, TimeUnit.SECONDS) // Set send delay with time unit
event("message", "Hello").id("1").retry(3000).delay(50) // Combine modifiers- JSON
{ "event": "message", "data": "Hello", "id": "1", "retry": 3000, "delay": 50 }With time unit:
{ "event": "message", "data": "Hello", "delay": { "duration": 1, "unit": "SECOND" } }Streaming Behavior
SSE responses are streamed over a persistent HTTP connection:
- Each event is sent as a separate HTTP chunk
- Events with
delay()are sent with the specified interval between them - Use
sse(...).delay(ms)to set a default delay for all events at once - Connection is closed after all events are sent
delayis a Moco mock feature, not part of the SSE wire format
Response Headers
SSE responses automatically include:
Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-aliveX-Accel-Buffering: no
Examples
LLM Token Streaming
- Java API
server.request(by(uri("/chat")))
.response(sse(
event("message", "Hello").delay(50),
event("message", " World").delay(50),
event("message", "!").delay(50)
));- JSON
{
"request" : {
"uri" : "/chat"
},
"response" : {
"sse" : [
{ "event": "message", "data": "Hello", "delay": 50 },
{ "event": "message", "data": " World", "delay": 50 },
{ "event": "message", "data": "!", "delay": 50 }
]
}
}Multiple Events with IDs
- Java API
server.request(by(uri("/events")))
.response(sse(
event("update", "First").id("001"),
event("update", "Second").id("002")
));- JSON
{
"request" : {
"uri" : "/events"
},
"response" : {
"sse" : [
{ "event": "update", "data": "First", "id": "001" },
{ "event": "update", "data": "Second", "id": "002" }
]
}
}Anonymous Data Events
- Java API
server.request(by(uri("/events")))
.response(sse(
data("first"),
data("second")
));- JSON
{
"request" : {
"uri" : "/events"
},
"response" : {
"sse" : [
{ "data": "first" },
{ "data": "second" }
]
}
}File-Based Events
- Java API
Create events.txt:
event: message
data: Hello
event: message
data: World
delay: 50Use it:
server.request(by(uri("/events")))
.response(sse(file("events.txt")));- JSON
{
"request" : {
"uri" : "/events"
},
"response" : {
"sse" : {
"file" : "events.txt"
}
}
}SSE Event File Format
event: message
data: first event
event: message
data: second event
delay: 50
id: 001
event: update
data: third eventFields:
id- Event identifierevent- Event type/namedata- Event data (can appear multiple times for multi-line data)retry- Reconnection time in milliseconds
Events are separated by blank lines.
File: moco-doc/usage.md
Usage
You have several ways to use Moco. One is API, which you can use in your unit test. The other is that run Moco as standalone. Currently, you put all your configuration in JSON file.
On the other hand, Moco has several different ways to integrate with some tools: Maven plugin, Gradle plugin and shell support.
Table of Contents
- API
- Standalone
- JSON configuration in Java API
- HTTPS
- JUnit Integration
- Maven Plugin
- Gradle Plugin
- Shell
- Scala
API
dependency
Moco has been published on Maven repository, so you can refer to it directly in your dependency. This is core dependency
<dependency>
<groupId>com.github.dreamhead</groupId>
<artifactId>moco-core</artifactId>
<version>1.6.1</version>
</dependency>A gradle example is as follow:
repositories {
mavenCentral()
}
dependencies {
testImplementation(
"com.github.dreamhead:moco-core:1.6.1",
)
}API example
Here is an typical Moco test case in JUnit.
import org.junit.jupiter.api.Test;
import java.io.IOException;
import com.github.dreamhead.moco.HttpServer;
import org.apache.http.client.fluent.Content;
import org.apache.http.client.fluent.Request;
import static com.github.dreamhead.moco.Moco.*;
import static com.github.dreamhead.moco.Runner.*;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.MatcherAssert.assertThat;
@Test
public void should_response_as_expected() throws Exception {
HttpServer server = httpServer(12306);
server.response("foo");
running(server, () -> {
Content content = Request.Get("http://localhost:12306").execute().returnContent();
assertThat(content.asString(), is("foo"));
});
}As shown above, we created a new server and configure it as expected. And then run our test against this server.
Here, We use Apache Http Client Fluent API to request our testing server.
Runner API
You may need to control server start/stop by yourself, for example, in your Before (or setup) method, runner API could be used.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import static com.github.dreamhead.moco.Moco.httpServer;
import static com.github.dreamhead.moco.Runner.runner;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class MocoRunnerTest {
private Runner runner;
@BeforeEach
public void setup() {
HttpServer server = httpServer(12306);
server.response("foo");
runner = runner(server);
runner.start();
}
@AfterEach
public void tearDown() {
runner.stop();
}
@Test
public void should_response_as_expected() throws IOException {
Content content = Request.Get("http://localhost:12306").execute().returnContent();
assertThat(content.asString(), is("foo"));
}
}Standalone
Moco can be used as standalone to run with configuration and you can download standalone directly:
Standalone Moco Runner
First of all, a JSON configuration file is required to start Moco.
[
{
"response" :
{
"text" : "foo"
}
}
](foo.json)
It's time to run Moco standalone server:
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.jsonNow, open your browser and input "http://localhost:12306". You will see "foo". That's it.
JSON configuration in Java API
@Since 0.10.0
If you have setup your server with JSON configuration, you can also run with your configuration from Java API.
import static com.github.dreamhead.moco.Moco.file;
import static com.github.dreamhead.moco.Moco.pathResource;
import static com.github.dreamhead.moco.MocoJsonRunner.jsonHttpServer;
import static com.github.dreamhead.moco.Runner.running;
import static com.github.dreamhead.moco.helper.RemoteTestUtils.port;
import static com.github.dreamhead.moco.helper.RemoteTestUtils.root;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class MocoJsonHttpRunnerTest extends AbstractMocoStandaloneTest {
@Test
public void should_return_expected_response() throws Exception {
final HttpServer server = jsonHttpServer(12306, file("foo.json"));
running(server, () -> {
Content content = Request.Get("http://localhost:12306").execute().returnContent();
assertThat(content.asString(), is("foo"));
});
}
}HTTPS
HTTPS is also a mainstream usage for HTTP protocol. Moco supports HTTPS as well. The main difference in API is that a certificate is required for HTTPS.
On the other hand, httpsServer should be used.
final HttpsCertificate certificate = certificate(pathResource("cert.jks"), "mocohttps", "mocohttps");
final HttpsServer server = httpsServer(certificate, hit);If you want to use HTTPS for standalone server. certificate information could be provided as CLI arguments.
java -jar moco-runner-<version>-standalone.jar https -p 12306 -c foo.json --https /path/to/cert.jks --cert mocohttps --keystore mocohttpsJUnit Integration
@Since 0.11.0
Moco provides JUnit integration to simplify the use of Moco in JUnit.
More details can be found in here.
Maven Plugin
Moco also can be used as Maven plugin.
https://github.com/GarrettHeel/moco-maven-plugin
Gradle Plugin
Moco can be used in Gradle
https://github.com/silverjava/moco-gradle-plugin
Shell
If you are using Mac or Linux, you may try the following approach:
- Make sure you have JDK 6 or later.
- Download the script.
- Place it on your $PATH. (~/bin is a good choice if it is on your path.)
- Set it to be executable. (chmod 755 ~/bin/moco)
Now, you can try
moco http -p 12306 -c foo.jsonIt will download the latest moco automatically if you don't have locally.
Scala
Scala fans can find Scala wrapper in
https://github.com/treppo/moco-scala
File: moco-doc/websocket-apis.md
Websocket API
Moco supports websocket api.
WARNING the json configuration below is just a snippet for one pair of request and response, instead of the whole configuration file.
Basis Usage
Websocket server is created from HTTP server.
HttpServer httpServer = httpServer(12306);
WebsocketServer server = httpServer.websocket("/ws");If you want to response according to request content, Moco server can be configured as following:
server.request(by("foo")).response("bar");You can also create websocket server in standalone api.
- JSON
{
"websocket": {
"uri": "/ws",
"sessions": [
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
}
]
}
}Connection Message
Client can receive Websocket server message while connection is established.
webSocketServer.connected("connected");{
"websocket": {
"uri": "/ws",
"connected": "connected"
}
}Ping/Pong
Ping/Pong is usually used as heartbeat in websocket.
websocketServer.ping("ping").pong("pong");{
"websocket": {
"uri": "/ws",
"pingpongs": [
{
"ping": "ping",
"pong": "pong"
}
]
}
}Broadcast
You can setup broadcast which means you broadcast your request to all client when you receive request.
server.request(by("foo")).response(broadcast("bar"));{
"request": {
"text": "foo"
},
"response": {
"broadcast": {
"content": "bar"
}
}
}If you just want to broadcast your message to some specific clients, group may help you. A client is supposed to join this group before your broadcast as following:
server.request(by("subscribe")).response(with("subscribed"), join(group("group")));
server.request(by("broadcast")).response(with("broadcasted"), broadcast("content", group("group")));[
{
"request": {
"text": "subscribe"
},
"response": {
"content": "subscribed",
"group": "group"
}
},
{
"request": {
"text": "broadcast"
},
"response": {
"content": "broadcasted",
"broadcast": {
"content": "content",
"group": "group"
}
}
}
]File: moco-junit5/src/test/resources/base.json
[
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
},
{
"response": {
"text": "blah"
}
}
]
File: moco-junit5/src/test/resources/foo.json
[
{
"description": "any response",
"response": {
"text": "foo"
}
},
{
"request": {
"uri": "/foo"
},
"response": {
"text": "bar"
}
},
{
"request": {
"uri": "/file"
},
"response": {
"file": "src/test/resources/foo.response"
}
},
{
"request": {
"text": "text_request"
},
"response": {
"text": "response_for_text_request"
}
},
{
"request": {
"file": "src/test/resources/foo.request"
},
"response": {
"text": "response_for_file_request"
}
},
{
"request": {
"uri": "/header",
"headers": {
"content-type": "application/json"
}
},
"response": {
"text": "response_for_header_request"
}
},
{
"request": {
"uri": "/query",
"queries": {
"param": "foo"
}
},
"response": {
"text": "response_for_query_request"
}
},
{
"request": {
"uri": "/xpath",
"xpaths": {
"/request/parameters/id/text()": "1"
}
},
"response": {
"text": "response_for_xpath_request"
}
},
{
"request": {
"uri": "/status"
},
"response": {
"status": 200
}
},
{
"request": {
"uri": "/response_header"
},
"response": {
"headers": {
"content-type": "application/json",
"foo": "bar"
}
}
},
{
"request": {
"uri": "/proxy"
},
"response": {
"proxy": "https://github.com/"
}
},
{
"request": {
"uri": "/composite-response"
},
"response": {
"status": 200,
"headers": {
"foo": "bar"
}
}
},
{
"request": {
"uri": "/latency"
},
"response": {
"latency": 1000
}
},
{
"request": {
"uri": "/latency-with-unit"
},
"response": {
"latency": {
"duration": 1,
"unit": "second"
}
}
},
{
"request": {
"uri": "/cookie",
"cookies": {
"login": "true"
}
},
"response": {
"text": "success"
}
},
{
"request": {
"uri": "/cookie"
},
"response": {
"cookies": {
"login": "true"
}
}
},
{
"request": {
"version": "HTTP/1.0"
},
"response": {
"text": "version"
}
},
{
"request": {
"uri": "/version10"
},
"response": {
"version": "HTTP/1.0"
}
}
]
File: moco-junit5/src/test/resources/rest.json
[
{
"resource": {
"name": "targets",
"post": [
{
"response": {
"status": 201,
"headers": {
"Location": "/targets/123"
}
}
}
]
}
}
]
File: moco-runner/src/test/resources/any_response_with_file.json
[
{
"response" :
{
"file" : "src/test/resources/foo.response"
}
}
]
File: moco-runner/src/test/resources/attachment.json
[
{
"request": {
"uri": "/text_attachment"
},
"response": {
"attachment": {
"filename": "text_attachment.txt",
"text": "text_attachment"
}
}
},
{
"request": {
"uri": "/file_attachment"
},
"response": {
"attachment": {
"filename": "file_attachment.txt",
"file": "src/test/resources/foo.response"
}
}
},
{
"request": {
"uri": "/path_attachment"
},
"response": {
"attachment": {
"filename": "path_attachment.txt",
"path_resource": "path.response"
}
}
}
]
File: moco-runner/src/test/resources/base.json
[
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
},
{
"response": {
"text": "blah"
}
}
]
File: moco-runner/src/test/resources/contain.json
[
{
"request": {
"uri": {
"contain": "foo"
}
},
"response": {
"text": "uri_match"
}
},
{
"request": {
"uri": "/text-match",
"text": {
"contain": "foo"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"foo": {
"contain" : "bar"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"contain" : "bar"
}
}
},
"response": {
"text": "query_match"
}
}
]
File: moco-runner/src/test/resources/cookie.json
[
{
"request": {
"uri": "/cookie"
},
"response": {
"cookies": {
"login": "true"
}
}
},
{
"request": {
"uri": "/cookie-with-path"
},
"response": {
"cookies": {
"login": {
"value": "true",
"path": "/"
}
}
}
},
{
"request": {
"uri": "/cookie-with-domain"
},
"response": {
"cookies": {
"login": {
"value": "true",
"domain": "github.com"
}
}
}
},
{
"request": {
"uri": "/cookie-with-secure"
},
"response": {
"cookies": {
"login": {
"value": "true",
"secure": "true"
}
}
}
},
{
"request": {
"uri": "/cookie-with-http-only"
},
"response": {
"cookies": {
"login": {
"value": "true",
"httpOnly": "true"
}
}
}
},
{
"request": {
"uri": "/cookie-with-max-age"
},
"response": {
"cookies": {
"login": {
"value": "true",
"maxAge": {
"duration": 1,
"unit": "hour"
}
}
}
}
},
{
"request": {
"uri": "/cookie-with-same-site"
},
"response": {
"cookies": {
"login": {
"value": "true",
"sameSite": "Lax"
}
}
}
}
]
File: moco-runner/src/test/resources/cors.json
[
{
"request": {
"uri": "/default-cors"
},
"response": {
"cors": true
}
},
{
"request": {
"uri": "/cors"
},
"response": {
"cors": {
"allowOrigin": "",
"allowMethods": [
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
],
"allowHeaders": [
"Content-Type, Authorization, Accept, X-Requested-With"
],
"maxAge": {
"duration": 1728000,
"unit": "second"
},
"exposeHeaders": [
"X-My-Custom-Header, X-Another-Custom-Header"
],
"allowCredentials": true
}
}
},
{
"request": {
"uri": "/cors-alias"
},
"response": {
"cors": {
"Access-Control-Allow-Origin": "",
"Access-Control-Allow-Methods": [
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
],
"Access-Control-Allow-Headers": [
"Content-Type, Authorization, Accept, X-Requested-With"
],
"Access-Control-Max-Age": {
"duration": 1728000,
"unit": "second"
},
"Access-Control-Expose-Headers": [
"X-My-Custom-Header, X-Another-Custom-Header"
],
"Access-Control-Allow-Credentials": true
}
}
}
]
File: moco-runner/src/test/resources/cycle.json
[
{
"request" : {
"uri" : "/cycle"
},
"response": {
"cycle": [
{
"text" : "foo"
},
{
"text" : "bar"
}
]
}
},
{
"request" : {
"uri" : "/cycle-json"
},
"response": {
"cycle": [
{
"json" : {
"foo" : "bar"
}
},
{
"json" : {
"hello" : "world"
}
}
]
}
}
]
File: moco-runner/src/test/resources/delete_method.json
[
{
"request" :
{
"method" : "delete",
"uri" : "/delete"
},
"response" :
{
"text" : "response_for_delete_method"
}
}
]
File: moco-runner/src/test/resources/ends_with.json
[
{
"request": {
"uri": {
"endsWith": "foo"
}
},
"response": {
"text": "uri_match"
}
},
{
"request": {
"uri": "/text-match",
"text": {
"endsWith": "foo"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"content-type": {
"endsWith" : "json"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"endsWith" : "r"
}
}
},
"response": {
"text": "query_match"
}
}
]
File: moco-runner/src/test/resources/event.json
[
{
"request": {
"uri": "/event"
},
"response": {
"text": "post_foo"
},
"on": {
"complete": {
"async": "true",
"latency": 500,
"post": {
"url": "http://localhost:12306/target",
"content": "content"
}
}
}
},
{
"request": {
"uri": "/event-with-unit"
},
"response": {
"text": "post_foo"
},
"on": {
"complete": {
"async": "true",
"latency": {
"duration": 1,
"unit": "second"
},
"post": {
"url": "http://localhost:12306/target",
"content": "content"
}
}
}
},
{
"request": {
"uri" : "/get_event"
},
"response": {
"text": "get_foo"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"get" : {
"url" : "http://localhost:12306/target"
}
}
}
},
{
"request": {
"uri" : "/get_event_with_header"
},
"response": {
"text": "get_foo_with_header"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"get" : {
"url" : "http://localhost:12306/target-with-header",
"headers": {
"foo": "bar"
}
}
}
}
},
{
"request": {
"uri" : "/post_event_with_header"
},
"response": {
"text": "post_foo_with_header"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"post" : {
"url" : "http://localhost:12306/target-with-header",
"headers": {
"foo": "bar"
},
"content": "content"
}
}
}
},
{
"request": {
"uri" : "/get_event_template"
},
"response": {
"text": "get_foo"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"get" : {
"url" : {
"template": {
"with": "http://localhost:12306/${target}",
"vars": {
"target" : "target"
}
}
}
}
}
}
},
{
"request": {
"uri": "/post-event-with-template-url"
},
"response": {
"text": "post_foo"
},
"on": {
"complete": {
"async": "true",
"latency": 500,
"post": {
"url" : {
"template": {
"with": "http://localhost:12306/${target}",
"vars": {
"target" : "target"
}
}
},
"content": "content"
}
}
}
},
{
"request": {
"uri": "/post-event-with-template-content"
},
"response": {
"text": "post_foo"
},
"on": {
"complete": {
"async": "true",
"latency": 500,
"post": {
"url": "http://localhost:12306/target",
"content": {
"template": {
"with": "${target}",
"vars": {
"target" : "target"
}
}
}
}
}
}
},
{
"request": {
"uri": "/event-with-json-post"
},
"response": {
"text": "post_json_foo"
},
"on": {
"complete": {
"async": "true",
"latency": 500,
"post": {
"url": "http://localhost:12306/target-json",
"json": {
"foo": "bar"
}
}
}
}
},
{
"request": {
"uri" : "/target"
},
"response": {
"text": "0XCAFEBABE"
}
},
{
"request": {
"uri" : "/target-json",
"json" : {
"foo" : "bar"
}
},
"response": {
"text": "0XMOCOJSON"
}
},
{
"request": {
"uri" : "/target-with-header",
"headers" : {
"foo" : "bar"
}
},
"response": {
"text": "0XMOCOHEADER"
}
}
]
File: moco-runner/src/test/resources/exist.json
[
{
"request": {
"uri": "/text-match",
"text": {
"exist": "true"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/text-not-match",
"text": {
"exist": "false"
}
},
"response": {
"text": "text_not_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"content-type": {
"exist" : "true"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/header-not-match",
"headers": {
"foo": {
"exist" : "false"
}
}
},
"response": {
"text": "header_not_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"exist" : "true"
}
}
},
"response": {
"text": "query_match"
}
},
{
"request": {
"uri": "/query-not-match",
"queries": {
"foo": {
"exist" : "false"
}
}
},
"response": {
"text": "query_not_match"
}
}
]
File: moco-runner/src/test/resources/foo.json
[
{
"description": "any response",
"response": {
"text": "foo"
}
},
{
"request": {
"uri": "/foo"
},
"response": {
"text": "bar"
}
},
{
"request": {
"uri": "/file"
},
"response": {
"file": "src/test/resources/foo.response"
}
},
{
"request": {
"text": "text_request"
},
"response": {
"text": "response_for_text_request"
}
},
{
"request": {
"file": "src/test/resources/foo.request"
},
"response": {
"text": "response_for_file_request"
}
},
{
"request": {
"uri": "/header",
"headers": {
"content-type": "application/json"
}
},
"response": {
"text": "response_for_header_request"
}
},
{
"request": {
"uri": "/query",
"queries": {
"param": "foo"
}
},
"response": {
"text": "response_for_query_request"
}
},
{
"request": {
"uri": "/xpath",
"xpaths": {
"/request/parameters/id/text()": "1"
}
},
"response": {
"text": "response_for_xpath_request"
}
},
{
"request": {
"uri": "/status"
},
"response": {
"status": 200
}
},
{
"request": {
"uri": "/response_header"
},
"response": {
"headers": {
"content-type": "application/json",
"foo": "bar"
}
}
},
{
"request": {
"uri": "/proxy"
},
"response": {
"proxy": "http://localhost:12306/foo"
}
},
{
"request": {
"uri": "/composite-response"
},
"response": {
"status": 200,
"headers": {
"foo": "bar"
}
}
},
{
"request": {
"uri": "/latency"
},
"response": {
"latency": 1000
}
},
{
"request": {
"uri": "/latency-with-unit"
},
"response": {
"latency": {
"duration": 1,
"unit": "second"
}
}
},
{
"request": {
"version": "HTTP/1.0"
},
"response": {
"text": "version"
}
},
{
"request": {
"uri": "/version10"
},
"response": {
"version": "HTTP/1.0"
}
}
]
File: moco-runner/src/test/resources/form.json
[
{
"request" : {
"method" : "post",
"forms" : {
"name" : "dreamhead"
}
},
"response" : {
"text" : "foobar"
}
}
]
File: moco-runner/src/test/resources/get_method.json
[
{
"request" :
{
"method" : "get",
"uri" : "/get"
},
"response" :
{
"text" : "response_for_get_method"
}
}
]
File: moco-runner/src/test/resources/header.json
[
{
"request" :
{
"uri" : "/header",
"headers" : {
"content-type" : "application/json"
}
},
"response" :
{
"text" : "response_for_header_request"
}
}
]
File: moco-runner/src/test/resources/json.json
[
{
"request": {
"uri": "/json",
"text": {
"json": "{"foo":"bar"}"
}
},
"response": {
"text": "response_for_json_request"
}
},
{
"request": {
"uri": "/json_shortcut",
"json": {
"foo": "bar"
}
},
"response": {
"text": "response_for_json_shortcut"
}
},
{
"request": {
"uri": "/json_response_shortcut"
},
"response": {
"json": {
"foo" : "bar"
}
}
}
]
File: moco-runner/src/test/resources/json_struct.json
[
{
"request": {
"uri": "/json-struct",
"struct": {
"json": {
"foo": "bar"
}
}
},
"response": {
"text": "response_for_json_struct"
}
}
]
File: moco-runner/src/test/resources/jsonpath.json
[
{
"request": {
"uri": "/jsonpath",
"json_paths": {
"$.book.price": "1"
}
},
"response": {
"text": "response_for_json_path_request"
}
}
]
File: moco-runner/src/test/resources/match.json
[
{
"request": {
"uri": {
"match": "/\w*/foo"
}
},
"response": {
"text": "uri_match"
}
},
{
"request": {
"uri": "/text-match",
"text": {
"match": "\wfoo"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/method-match",
"method": {
"match": "get|post"
}
},
"response": {
"text": "method_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"content-type": {
"match" : "application/\w"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"match" : "b\w*"
}
}
},
"response": {
"text": "query_match"
}
}
]
File: moco-runner/src/test/resources/mount.json
[
{
"mount" :
{
"dir" : "src/test/resources/mount",
"uri" : "/mount"
}
},
{
"mount" :
{
"dir" : "src/test/resources/mount",
"uri" : "/mount-include",
"includes" :
[
".response"
]
}
},
{
"mount" :
{
"dir" : "src/test/resources/mount",
"uri" : "/mount-exclude",
"excludes" :
[
".response"
]
}
},
{
"mount" :
{
"dir" : "src/test/resources/mount",
"uri" : "/mount-response",
"headers" : {
"Content-Type" : "text/plain"
}
}
}
]
File: moco-runner/src/test/resources/path.json
[
{
"request": {
"uri": {
"path": "/path/{path}"
}
},
"response": {
"text": "path"
}
},
{
"request": {
"uri": {
"path": "/path/{path}/sub/{sub}"
}
},
"response": {
"text": "sub"
}
}
]
File: moco-runner/src/test/resources/path_resource.json
[
{
"request": {
"uri": "/path-resource"
},
"response": {
"path_resource": "path.response"
}
},
{
"request": {
"path_resource": "path.request"
},
"response": {
"text": "path resource"
}
}
]
File: moco-runner/src/test/resources/post_method.json
[
{
"request" :
{
"method" : "post",
"uri" : "/post"
},
"response" :
{
"text" : "response_for_post_method"
}
}
]
File: moco-runner/src/test/resources/proxy.json
[
{
"request" :
{
"uri" : "/target"
},
"response" :
{
"text" : "proxy_target"
}
},
{
"request" :
{
"uri" : "/sse-target"
},
"response" :
{
"sse" : [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": "World" }
]
}
},
{
"request" :
{
"uri" : "/sse-delay-target"
},
"response" :
{
"sse" : [
{ "event": "message", "data": "first", "delay": 100 },
{ "event": "message", "data": "second", "delay": 100 }
]
}
},
{
"request" :
{
"uri" : "/proxy"
},
"response" :
{
"proxy" : "http://localhost:12306/target"
}
},
{
"request" :
{
"uri" : "/sse-proxy"
},
"response" :
{
"proxy" : "http://localhost:12306/sse-target"
}
},
{
"request" :
{
"uri" : "/sse-proxy-delay"
},
"response" :
{
"proxy" : "http://localhost:12306/sse-delay-target"
}
},
{
"request" :
{
"uri" : "/failover"
},
"response" :
{
"proxy" :
{
"url" : "http://localhost:12306/unknown",
"failover" : "src/test/resources/standalone_failover.json"
}
}
},
{
"request" :
{
"uri" : "/playback"
},
"response" :
{
"proxy" :
{
"url" : "http://localhost:12306/unknown",
"playback" : "src/test/resources/standalone_failover.json"
}
}
},
{
"request" :
{
"uri" : "/template-url"
},
"response" :
{
"proxy" :
{
"url" : {
"template": "http://localhost:12306/${req.queries['foo']}"
}
}
}
},
{
"request" :
{
"uri" : "/failover-target"
},
"response" :
{
"seq" : [
{
"status": 404
},
{
"status": 500
}
]
}
},
{
"request" :
{
"uri" : "/failover-with-status"
},
"response" :
{
"proxy" :
{
"url" : "http://localhost:12306/failover-target",
"failover" : {
"file": "src/test/resources/standalone_failover.json",
"status": [404, 500]
}
}
}
}
]
File: moco-runner/src/test/resources/proxy_batch.json
[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"request" :
{
"uri" : {
"match" : "/proxy/."
}
},
"response" :
{
"proxy" : {
"from" : "/proxy",
"to" : "http://localhost:12306/target"
}
}
},
{
"request" :
{
"uri" : {
"match" : "/failover/."
}
},
"response" :
{
"proxy" :
{
"from" : "/failover",
"to" : "http://localhost:12306/unknown",
"failover" : "src/test/resources/standalone_failover.json"
}
}
}
]
File: moco-runner/src/test/resources/proxy_error_from_missing.json
[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"request" :
{
"uri" : {
"match" : "/proxy/.*"
}
},
"response" :
{
"proxy" : {
"to" : "http://localhost:12306/target"
}
}
}
]
File: moco-runner/src/test/resources/proxy_error_multiple_mode.json
[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"request" :
{
"uri" : {
"match" : "/proxy/.*"
}
},
"response" :
{
"proxy" : {
"url" : "http://localhost:12306/target/1",
"from" : "/proxy",
"to" : "http://localhost:12306/target"
}
}
}
]
File: moco-runner/src/test/resources/proxy_error_to_missing.json
[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"request" :
{
"uri" : {
"match" : "/proxy/.*"
}
},
"response" :
{
"proxy" : {
"from" : "/proxy"
}
}
}
]
File: moco-runner/src/test/resources/proxy_error_url_from_server.json
[
{
"proxy" : {
"url" : "http://localhost:12306/target"
}
}
]
File: moco-runner/src/test/resources/proxy_server.json
[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"proxy" : {
"from" : "/proxy",
"to" : "http://localhost:12306/target"
}
},
{
"proxy" :
{
"from" : "/failover",
"to" : "http://localhost:12306/unknown",
"failover" : "src/test/resources/standalone_failover.json"
}
}
]
File: moco-runner/src/test/resources/proxy_sse_failover.json
[
{
"request": {
"uri": "/sse-target"
},
"response": {
"sse": [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": "World" }
]
}
},
{
"request": {
"uri": "/sse-proxy-failover"
},
"response": {
"proxy": {
"url": "http://localhost:12306/sse-target",
"failover": "src/test/resources/standalone_sse_failover.json"
}
}
},
{
"request": {
"uri": "/sse-proxy-playback"
},
"response": {
"proxy": {
"url": "http://localhost:12306/unknown",
"playback": "src/test/resources/standalone_sse_failover.json"
}
}
}
]
File: moco-runner/src/test/resources/proxy_sse_failover_delay.json
[
{
"request": {
"uri": "/sse-delay-target"
},
"response": {
"sse": [
{ "event": "message", "data": "first", "delay": 100 },
{ "event": "message", "data": "second", "delay": 100 }
]
}
},
{
"request": {
"uri": "/sse-proxy-delay"
},
"response": {
"proxy": {
"url": "http://localhost:12306/sse-delay-target",
"failover": "src/test/resources/standalone_sse_failover_delay.json"
}
}
}
]
File: moco-runner/src/test/resources/proxy_sse_failover_partial.json
[
{
"request": {
"uri": "/sse-target-partial"
},
"response": {
"sse": [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": "World", "delay": 30000 }
]
}
},
{
"request": {
"uri": "/sse-proxy-partial"
},
"response": {
"proxy": {
"url": "http://localhost:12306/sse-target-partial",
"failover": "src/test/resources/standalone_sse_failover_partial.json"
}
}
}
]
File: moco-runner/src/test/resources/put_method.json
[
{
"request" :
{
"method" : "put",
"uri" : "/put"
},
"response" :
{
"text" : "response_for_put_method"
}
}
]
File: moco-runner/src/test/resources/query.json
[
{
"request" :
{
"uri" : "/query",
"queries" : {
"param" : "foo"
}
},
"response" :
{
"text" : "response_for_query_request"
}
},
{
"request" :
{
"uri" : "/empty-query",
"queries" : {
"param" : ""
}
},
"response" :
{
"text" : "response_for_empty_query_request"
}
},
{
"request" :
{
"uri" : "/multi-query",
"queries" : {
"param1" : "foo",
"param2" : "bar"
}
},
"response" :
{
"text" : "response_for_multi_query_request"
}
}
]
File: moco-runner/src/test/resources/record_replay.json
[
{
"request" : {
"uri" : "/record"
},
"response" : {
"record" : "name"
}
},
{
"request" : {
"uri" : "/replay"
},
"response" : {
"replay" : "name"
}
},
{
"request" : {
"uri" : "/record-template"
},
"response" : {
"record" : {
"identifier": {
"template": "${req.queries['type']}"
}
}
}
},
{
"request" : {
"uri" : "/replay-template"
},
"response" : {
"replay" : {
"identifier": {
"template": "${req.queries['type']}"
}
}
}
},
{
"request" : {
"uri" : "/record-modifier"
},
"response" : {
"record" : {
"identifier": "modifier"
}
}
},
{
"request" : {
"uri" : "/replay-modifier"
},
"response" : {
"replay" : {
"identifier": "modifier",
"modifier": "${req.queries['type']}"
}
}
},
{
"request" : {
"uri" : "/record-group"
},
"response" : {
"record" : {
"group": "group",
"identifier": "modifier"
}
}
},
{
"request" : {
"uri" : "/replay-group"
},
"response" : {
"replay" : {
"group": "group",
"identifier": "modifier"
}
}
},
{
"request" : {
"uri" : "/record-tape"
},
"response" : {
"record" : {
"group": "tape-foo",
"tape": "src/test/resources/tape.json"
}
}
},
{
"request" : {
"uri" : "/replay-tape"
},
"response" : {
"replay" : {
"group": "tape-foo",
"tape": "src/test/resources/tape.json"
}
}
},
{
"request" : {
"uri" : "/record-modifier-with-header"
},
"response" : {
"record" : {
"identifier": "modifier"
}
}
},
{
"request" : {
"uri" : "/replay-modifier-with-header"
},
"response" : {
"replay" : {
"identifier": "modifier",
"modifier": {
"headers": {
"X-REPLAY": {
"template": "${req.queries['type']}"
}
}
}
}
}
}
]
File: moco-runner/src/test/resources/redirect.json
[
{
"request" :
{
"uri" : "/redirectTo"
},
"response" :
{
"text" : "foo"
}
},
{
"request" :
{
"uri" : "/redirect"
},
"redirectTo" : "http://localhost:12306/redirectTo"
},
{
"request" :
{
"uri" : "/redirect-with-template"
},
"redirectTo" : {
"template" : {
"with" : "${url}",
"vars" : {
"url" : "http://localhost:12306/redirectTo"
}
}
}
},
{
"request" :
{
"uri" : "/redirect-with-path-resource"
},
"redirectTo" : {
"path_resource": "redirect.url"
}
}]
File: moco-runner/src/test/resources/response_with_file_and_charset.json
[
{
"response":
{
"file":
{
"name": "src/test/resources/foo.response",
"charset": "utf-8"
}
}
},
{
"request":
{
"uri":"/charset_first"
},
"response":
{
"file":
{
"charset": "utf-8",
"name": "src/test/resources/foo.response"
}
}
}
]
File: moco-runner/src/test/resources/response_with_path_resource_and_charset.json
[
{
"response":
{
"path_resource":
{
"name": "path.response",
"charset": "utf-8"
}
}
}
]
File: moco-runner/src/test/resources/response_with_template_name.json
[
{
"response":
{
"file":
{
"name": {
"template": {
"with" : "src/test/resources/${var}.response",
"vars" : {
"var" : "foo"
}
}
}
}
}
}
]
File: moco-runner/src/test/resources/seq.json
[
{
"request" : {
"uri" : "/seq"
},
"response": {
"seq": [
{
"text" : "foo"
},
{
"text" : "bar"
}
]
}
},
{
"request" : {
"uri" : "/seq-json"
},
"response": {
"seq": [
{
"json" : {
"foo" : "bar"
}
},
{
"json" : {
"hello" : "world"
}
}
]
}
}
]
File: moco-runner/src/test/resources/sse.json
[
{
"request" : {
"uri" : "/sse"
},
"response" : {
"sse" : {
"file" : "src/test/resources/events.sse"
}
}
},
{
"request" : {
"uri" : "/inline"
},
"response" : {
"sse" : [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": " World" }
]
}
},
{
"request" : {
"uri" : "/delay"
},
"response" : {
"sse" : [
{ "event": "message", "data": "token1", "delay": 100 },
{ "event": "message", "data": "token2", "delay": 100 },
{ "event": "message", "data": "token3", "delay": 100 }
]
}
},
{
"request" : {
"uri" : "/sse-delay"
},
"response" : {
"sse" : {
"delay": 100,
"events": [
{ "event": "message", "data": "token1" },
{ "event": "message", "data": "token2" },
{ "event": "message", "data": "token3" }
]
}
}
},
{
"request" : {
"uri" : "/sse-delay-unit"
},
"response" : {
"sse" : {
"delay": { "duration": 100, "unit": "MILLISECOND" },
"events": [
{ "event": "message", "data": "token1" },
{ "event": "message", "data": "token2" },
{ "event": "message", "data": "token3" }
]
}
}
},
{
"request" : {
"uri" : "/event-delay-unit"
},
"response" : {
"sse" : [
{ "event": "message", "data": "token1", "delay": { "duration": 100, "unit": "MILLISECOND" } },
{ "event": "message", "data": "token2", "delay": { "duration": 100, "unit": "MILLISECOND" } },
{ "event": "message", "data": "token3" }
]
}
}
]
File: moco-runner/src/test/resources/standalone_failover.json
[ {
"request" : {
"version" : "HTTP/1.1",
"headers" : {
"Host" : "localhost:12306"
}
},
"response" : {
"version" : "HTTP/1.1",
"content" : "proxy",
"headers" : {
"Content-Length" : "5",
"Content-Type" : "text/plain; charset=UTF-8"
},
"status" : 200
}
} ]
File: moco-runner/src/test/resources/standalone_sse_failover.json
[ {
"request" : {
"content" : null,
"headers" : {
"Host" : [ "localhost:12306" ]
},
"method" : "GET",
"queries" : null,
"uri" : "/sse-proxy-playback",
"version" : "HTTP/1.1"
},
"response" : {
"content" : null,
"headers" : {
"Content-Type" : [ "text/event-stream" ]
},
"sseEvents" : [ {
"id" : null,
"event" : "message",
"data" : [ "Hello" ],
"retry" : null,
"delay" : 0
}, {
"id" : null,
"event" : "message",
"data" : [ "World" ],
"retry" : null,
"delay" : 0
} ],
"status" : 200,
"version" : "HTTP/1.1"
}
}, {
"request" : {
"content" : "",
"headers" : {
"content-length" : [ "0" ],
"Connection" : [ "keep-alive" ],
"Host" : [ "localhost:12306" ],
"Accept-Encoding" : [ "gzip, x-gzip, deflate" ]
},
"method" : "GET",
"queries" : { },
"uri" : "/sse-proxy-failover",
"version" : "HTTP/1.1"
},
"response" : {
"content" : null,
"headers" : { },
"sseEvents" : [ {
"id" : null,
"event" : "message",
"data" : [ "Hello" ],
"retry" : null,
"delay" : 0
}, {
"id" : null,
"event" : "message",
"data" : [ "World" ],
"retry" : null,
"delay" : 0
} ],
"status" : 200,
"version" : "HTTP/1.1"
}
} ]
File: moco-runner/src/test/resources/standalone_sse_failover_delay.json
[ {
"request" : {
"content" : "",
"headers" : {
"content-length" : [ "0" ],
"Connection" : [ "keep-alive" ],
"Host" : [ "localhost:12306" ],
"Accept-Encoding" : [ "gzip, x-gzip, deflate" ]
},
"method" : "GET",
"queries" : { },
"uri" : "/sse-proxy-delay",
"version" : "HTTP/1.1"
},
"response" : {
"content" : null,
"headers" : { },
"sseEvents" : [ {
"id" : null,
"event" : "message",
"data" : [ "first" ],
"retry" : null,
"delay" : 0
}, {
"id" : null,
"event" : "message",
"data" : [ "second" ],
"retry" : null,
"delay" : 0
} ],
"status" : 200,
"version" : "HTTP/1.1"
}
} ]
File: moco-runner/src/test/resources/standalone_sse_failover_partial.json
[ {
"request" : {
"content" : "",
"headers" : {
"content-length" : [ "0" ],
"Connection" : [ "keep-alive" ],
"Host" : [ "localhost:12306" ],
"Accept-Encoding" : [ "gzip, x-gzip, deflate" ]
},
"method" : "GET",
"queries" : { },
"uri" : "/sse-proxy-partial",
"version" : "HTTP/1.1"
},
"response" : {
"content" : null,
"headers" : { },
"sseEvents" : [ {
"id" : null,
"event" : "message",
"data" : [ "Hello" ],
"retry" : null,
"delay" : 0
} ],
"status" : 200,
"version" : "HTTP/1.1"
}
} ]
File: moco-runner/src/test/resources/starts_with.json
[
{
"request": {
"uri": {
"startsWith": "/foo"
}
},
"response": {
"text": "uri_match"
}
},
{
"request": {
"uri": "/text-match",
"text": {
"startsWith": "foo"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"content-type": {
"startsWith" : "application"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"startsWith" : "b"
}
}
},
"response": {
"text": "query_match"
}
}
]
File: moco-runner/src/test/resources/tape.json
{
"content" : {
"tape-foo" : {
"content" : {
"content" : "Zm9v",
"charset" : "UTF-8"
},
"headers" : {
"Connection" : [ "keep-alive" ],
"Host" : [ "localhost:12306" ],
"Accept-Encoding" : [ "gzip, x-gzip, deflate" ],
"Content-Length" : [ "3" ],
"Content-Type" : [ "text/plain; charset=UTF-8" ]
},
"method" : "POST",
"queries" : { },
"uri" : "/record-tape",
"version" : "HTTP/1.1"
}
}
}
File: moco-runner/src/test/resources/template.json
[
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.method}"
}
}
},
{
"request": {
"uri": "/file_template"
},
"response": {
"file": {
"template": "src/test/resources/template.foo"
}
}
},
{
"request": {
"uri": "/path_resource_template"
},
"response": {
"path_resource": {
"template": "template.foo"
}
}
},
{
"request": {
"uri": "/version_template"
},
"response": {
"version": {
"template": "${req.version}"
}
}
},
{
"request": {
"uri": "/header_template"
},
"response": {
"headers": {
"foo": {
"template": "${req.headers['foo']}"
}
}
}
},
{
"request": {
"uri": "/cookie_template",
"cookies": {
"foo": "GET"
}
},
"response": {
"text": {
"template": "${req.cookies['foo']}"
}
}
},
{
"request": {
"uri": "/cookie_template"
},
"response": {
"cookies": {
"foo": {
"template": "${req.method}"
}
},
"status": 302
}
},
{
"request": {
"uri": "/form_template"
},
"response": {
"text": {
"template": "${req.forms['foo']}"
}
}
},
{
"request": {
"uri": "/query_template"
},
"response": {
"text": {
"template": "${req.queries['foo']}"
}
}
},
{
"request": {
"uri": "/json_template"
},
"response": {
"text": {
"template": "${req.json.foo}"
}
}
},
{
"request": {
"uri": "/xml_template"
},
"response": {
"text": {
"template": "${req.xml.foo}"
}
}
},
{
"request": {
"uri": "/client_address_template"
},
"response": {
"text": {
"template": "${req.client.address}"
}
}
},
{
"request": {
"uri": "/client_port_template"
},
"response": {
"text": {
"template": "${req.client.port}"
}
}
},
{
"request": {
"uri": {
"path": "/path/{path}"
}
},
"response": {
"text": {
"template": "${req.path.path}"
}
}
}
]
File: moco-runner/src/test/resources/template_with_extractor.json
[
{
"request": {
"uri": "/extractor_template"
},
"response": {
"text": {
"template": {
"with" : "${foo}",
"vars" : {
"foo" : {
"json_path": "$.book.price"
}
}
}
}
}
}
]
File: moco-runner/src/test/resources/template_with_function.json
[
{
"request": {
"uri": "/now_template"
},
"response": {
"text": {
"template": "${now("yyyy-MM-dd")}"
}
}
},
{
"request": {
"uri": "/random_template_with_range_and_format"
},
"response": {
"text": {
"template": "${random(100, "###.######")}"
}
}
},
{
"request": {
"uri": "/random_template_with_range"
},
"response": {
"text": {
"template": "${random(100)}"
}
}
},
{
"request": {
"uri": "/random_template_with_format"
},
"response": {
"text": {
"template": "${random("###.######")}"
}
}
},
{
"request": {
"uri": "/random_template_without_arg"
},
"response": {
"text": {
"template": "${random()}"
}
}
}
]
File: moco-runner/src/test/resources/template_with_var.json
[
{
"request": {
"uri": "/var_template"
},
"response": {
"text": {
"template": {
"with" : "${foo} ${bar}",
"vars" : {
"foo" : "another",
"bar" : "template"
}
}
}
}
}
]
File: moco-runner/src/test/resources/xml.json
[
{
"request": {
"uri": "/xml",
"text": {
"xml": "
}
},
"response": {
"text": "response_for_xml_request"
}
},
{
"request": {
"uri": "/xmlfile",
"file": {
"xml": "src/test/resources/xml.xml"
}
},
"response": {
"text": "response_for_xml_file_request"
}
}
]
File: moco-runner/src/test/resources/xml_struct.json
[
{
"request": {
"uri": "/struct-xml",
"struct": {
"xml": "
}
},
"response": {
"text": "response_for_xml_struct_request"
}
}
]
File: moco-runner/src/test/resources/xpath.json
[
{
"request" :
{
"uri" : "/xpath",
"xpaths" : {
"/request/parameters/id/text()" : "1"
}
},
"response" :
{
"text" : "response_for_xpath_request"
}
}
]
File: moco-runner/src/test/resources/rest/rest.json
[
{
"resource": {
"name": "targets",
"get": [
{
"id": "1",
"request": {
"headers": {
"Content-Type": "application/json"
}
},
"response": {
"json": {
"code": 1,
"message": "foo"
}
}
},
{
"id": "2",
"response": {
"json": {
"code": 2,
"message": "bar"
}
}
}
],
"post": [
{
"response": {
"status": 201,
"headers": {
"Location": "/targets/123"
}
}
}
],
"put": [
{
"id": "1",
"response": {
"status": 200
}
}
],
"delete": [
{
"id": "1",
"response": {
"status": 200
}
}
],
"head": [
{
"id": "1",
"response": {
"status": 200,
"headers": {
"ETag": "Moco"
}
}
}
],
"patch": [
{
"id": "1",
"response": {
"text": "patch result"
}
}
],
"resource": [
{
"id": "1",
"name": "subs",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 3,
"message": "sub"
}
}
}
],
"resource": [
{
"id": "1",
"name": "sub-subs",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 4,
"message": "sub-sub"
}
}
}
]
}
]
},
{
"id": "",
"name": "any-subs",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 4,
"message": "any-sub"
}
}
}
]
}
]
}
},
{
"resource": {
"name": "any-targets",
"get": [
{
"id": "",
"request": {
"headers": {
"Content-Type": "application/json"
}
},
"response": {
"json": {
"code": 1,
"message": "any"
}
}
}
],
"resource": [
{
"id": "",
"name": "any-subs",
"get": [
{
"id": "",
"response": {
"json": {
"code": 100,
"message": "any-sub"
}
}
}
]
}
]
}
},
{
"resource": {
"name": "all-resources",
"get": [
{
"response": {
"json": [
{
"code": 1000,
"message": "all-1"
},
{
"code": 100,
"message": "all-2"
}
]
}
}
],
"head": [
{
"response": {
"headers" : {
"ETag": "Moco"
}
}
}
]
}
}
]
File: moco-runner/src/test/resources/rest/rest_error_without_delete_id.json
[
{
"resource" : {
"name" : "error",
"delete": [
{
"response": {
"status": 200
}
}
]
}
}
]
File: moco-runner/src/test/resources/rest/rest_error_without_patch_id.json
[
{
"resource" : {
"name" : "error",
"patch": [
{
"response": {
"status": 200
}
}
]
}
}
]
File: moco-runner/src/test/resources/rest/rest_error_without_put_id.json
[
{
"resource" : {
"name" : "error",
"put": [
{
"response": {
"status": 200
}
}
]
}
}
]
File: moco-runner/src/test/resources/rest/rest_error_without_response.json
[
{
"resource": {
"name": "targets",
"get": [
{
"id": "1"
}
]
}
}
]
File: moco-runner/src/test/resources/settings/context-settings.json
[
{
"context": "/foo",
"include": "src/test/resources/settings/details/foo.json"
},
{
"context": "/bar",
"include": "src/test/resources/settings/details/bar.json"
}
]
File: moco-runner/src/test/resources/settings/env-settings.json
[
{
"context": "/foo",
"env": "foo",
"include": "src/test/resources/settings/details/foo.json"
},
{
"context": "/bar",
"env": "bar",
"include": "src/test/resources/settings/details/bar.json"
}
]
File: moco-runner/src/test/resources/settings/fileroot-settings.json
[
{
"context": "/fileroot",
"file_root": "src/test/resources",
"include": "settings/fileroot.json"
}
]
File: moco-runner/src/test/resources/settings/fileroot.json
[
{
"request" : {
"uri" : "/fileroot"
},
"response" : {
"file" : "foo.response"
}
}
]
File: moco-runner/src/test/resources/settings/glob-settings.json
[
{
"include" : "src/test/resources/settings/details/*.json"
}
]
File: moco-runner/src/test/resources/settings/request-settings.json
[
{
"request" : {
"headers" : {
"foo" : "bar"
}
},
"include": "src/test/resources/settings/details/foo.json"
}
]
File: moco-runner/src/test/resources/settings/response-settings.json
[
{
"response" : {
"headers" : {
"foo" : "bar"
}
},
"include": "src/test/resources/settings/details/foo.json"
}
]
File: moco-runner/src/test/resources/settings/settings.json
[
{
"include" : "src/test/resources/settings/details/foo.json"
},
{
"include" : "src/test/resources/settings/details/bar.json"
}
]
File: moco-runner/src/test/resources/settings/details/bar.json
[
{
"request" : {
"uri" : "/bar"
},
"response" : {
"text" : "bar"
}
}
]
File: moco-runner/src/test/resources/settings/details/foo.json
[
{
"request" : {
"uri" : "/foo"
},
"response" : {
"text" : "foo"
}
}
]
File: moco-runner/src/test/resources/websocket/websocket.json
[
{
"websocket": {
"uri": "/ws",
"connected": "connected",
"pingpongs": [
{
"ping": "ping",
"pong": "pong"
}
],
"sessions": [
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
},
{
"request": {
"file": "src/test/resources/foo.request"
},
"response": {
"file": "src/test/resources/foo.response"
}
}
]
}
}
]
File: moco-runner/src/test/resources/websocket/websocket_with_broadcast.json
[
{
"websocket": {
"uri": "/ws",
"sessions": [
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
},
{
"request": "broadcast",
"response": {
"text": "subcribed",
"broadcast": {
"content": "login"
}
}
},
{
"request": "broadcast2",
"response": {
"text": "broadcast2",
"broadcast": {
"content": "login2"
}
}
},
{
"request": "broadcast-with-file",
"response": {
"text": "broadcast-with-file",
"broadcast": {
"content": {
"file": "src/test/resources/foo.response"
}
}
}
},
{
"request": "subscribe-with-group",
"response": {
"text": "subscribed",
"group": "group"
}
},
{
"request": "broadcast-with-group",
"response": {
"text": "subscribed",
"broadcast": {
"group" : "group",
"content": "broadcast-content"
}
}
}
]
}
}
]
File: .github/workflows/build.yaml
name: CI
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java-version: ["17", "21", "25"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: '${{ matrix.java-version }}'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run checks
run: ./gradlew check -i
- name: Run ProGuard check
run: ./gradlew :moco-runner:proguardCheck -i
- name: Upload test reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-reports-java-${{ matrix.java-version }}
path: |
**/build/reports/tests/
**/build/test-results/