<a href="https://goreportcard.com/report/github.com/checkr/openmock" target="_blank">
<img src="https://goreportcard.com/badge/github.com/checkr/openmock">
</a>
<a href="https://circleci.com/gh/checkr/openmock" target="_blank">
<img src="https://circleci.com/gh/checkr/openmock.svg?style=shield">
</a>
<a href="https://godoc.org/github.com/checkr/openmock" target="_blank">
<img src="https://img.shields.io/badge/godoc-reference-green.svg">
</a>
OpenMock is a Go service that can mock services in integration tests, staging environment, or anywhere. The goal is to simplify the process of writing mocks in various channels. Currently it supports the following channels:
Use it with docker.
$ docker run -it -p 9999:9999 -v $(pwd)/demo_templates:/data/templates checkr/openmock
More complete openmock instance (e.g. redis) with docker-compose.
$ docker-compose up
Test it.
$ curl localhost:9999/ping
Dependencies.
OPENMOCK_HTTP_PORT=80OPENMOCK_GRPC_PORT=50051OPENMOCK_KAFKA_ENABLED=true.OPENMOCK_KAFKA_SEED_BROKERS, OPENMOCK_KAFKA_PRODUCER_SEED_BROKERS, and OPENMOCK_KAFKA_CONSUMER_SEED_BROKERSOPENMOCK_KAFKA_SEED_BROKERSOPENMOCK_KAFKA_SASL_USERNAMEOPENMOCK_KAFKA_SASL_PASSWORDOPENMOCK_KAFKA_TLS_ENABLEDOPENMOCK_AMQP_ENABLED=trueOPENMOCK_AMQP_URL.Templates are YAML files that describe the behavior of OpenMock.
You can put any number of .yaml or .yml files in a directory, and then point
environment variable OPENMOCK_TEMPLATES_DIR to it. OpenMock
will recursively (including subdirectories) load all the YAML files. For example:
# OPENMOCK_TEMPLATES_DIR=./demo_templates
./demo_templates
├── amqp.yaml
├── files
│ └── colors.json
├── http.yaml
├── jsonrpc.yaml
├── kafka.yaml
└── payload_from_file.yaml
OpenMock is configured a list of behaviors for it to follow. Each behavior is identified by a key, and a kind:
- key: respond-to-resource
kind: Behavior
It represents the channel to listen on and condition for the actions of the behavior to be performed. Available channels are:
For example, under what condition and from what channel should we proceed with the actions.
- key: no-op
kind: Behavior
expect:
# Condition checks if we need to do the actions or not
# It only proceeds if it evaluates to "true"
condition: '{{.HTTPHeader.Get "X-Token" | eq "t1234"}}'
# Use one (and only one) of the following channels - [http, kafka, amqp]
http:
method: GET
path: /ping
kafka:
topic: hello_kafka_in
amqp:
exchange: exchange_1
routing_key: key_in
queue: key_in
Actions are a series of functions to run. Availabe actions are:
- key: every-op
kind: Behavior
expect:
http:
method: GET
path: /ping
actions:
- publish_kafka:
topic: hello_kafka_out
payload: >
{
"kafka": "OK",
"data": {}
}
- sleep:
duration: 1s
- reply_http:
status_code: 200
body: OK
headers:
Content-Type: text/html
The actions by default run in the order defined in the mock file; you can adjust this by adding an int 'order' value from lowest to highest number. The default value for 'order' is 0.
- key: every-op
kind: Behavior
expect:
http:
method: GET
path: /ping
actions:
- publish_kafka:
topic: hello_kafka_out
payload: >
{
"kafka": "OK",
"data": {}
}
- sleep:
duration: 1s
# sleep first
order: -1000
Templates can be useful to assemble your payloads from parts
- key: dog
kind: Template
template: >
<animal>dog</animal>
- key: cat
kind: Template
template: >
<animal>cat</animal>
# $ curl 0:9999/fred
# <human> <name>fred</name> <pets> <animal>dog</animal> <animal>cat</animal> </pets> </human>
- key: get-freds-pets
kind: Behavior
expect:
http:
method: GET
path: /fred
actions:
- reply_http:
status_code: 200
body: >
<human>
<name>fred</name>
<pets>
{{template "dog"}}
{{template "cat"}}
</pets>
</human>
Abstract Behaviors can be used to parameterize some data.
When an abstract behavior and a behavior extending it both have actions defined, all of them are run when the behavior matches. Actions will run from lowest to highest value of the 'order' field; if this is the same for two actions the action defined earlier in the abstract behavior runs first, followed by actions in the concrete behavior.
Be aware that values with all digits will be interpreted into int type (YAML syntax), and it will fail the condition check given that some helper functions are returning string types. Pipe to toString before the comparison or alternatively put quotes around the values. See example in abstract_behaviors.yml.
- key: fruit-of-the-day
kind: AbstractBehavior
values:
fruit: potato
expect:
condition: '{{.HTTPQueryString | contains .Values.day}}'
http:
method: GET
path: /fruit-of-the-day
actions:
- reply_http:
status_code: 200
body: '{"fruit": "{{.Values.fruit}}"}'
# $ curl 0:9999/fruit-of-the-day?day=monday
# {"fruit": "apple"}
- key: monday-fruit
kind: Behavior
extend: fruit-of-the-day
values:
day: monday
fruit: apple
# $ curl 0:9999/fruit-of-the-day?day=tuesday
# {"fruit": "potato"}
- key: tuesday-fruit
kind: Behavior
extend: fruit-of-the-day
values:
day: tuesday
actions:
# sleep then reply_http
- sleep:
duration: 1s
order: -1000
OpenMock leverages https://golang.org/pkg/text/template/ to write dynamic templates. Specifically, it supports a lot of Context and Helper Functions.
Usage of {{ expr }}. One can put {{ expr }} inside three types of places:
expect.condition
action.http.body, action.grpc.payload, action.kafka.payload, action.amqp.payloadaction.http.body_from_file, action.http.body_from_binary_file, action.http.binary_file_name ,action.grpc.payload_from_file, action.kafka.payload_from_file, action.amqp.payload_from_file ({{ expr }} will be in the file)
Use Context inside {{ expr }}.
```bash .HTTPHeader # type: http.Header; example: {{.HTTPHeader.Get "X-Token"}} .HTTPBody # type: string; example: {{.HTTPBody}} .HTTPPath # type: string; example: {{.HTTPPath}} .HTTPQueryString # type: string; example: {{.HTTPQueryString}}
.GRPCHeader # type: string; example: {{.GRPCHeader}} .GRPCPayload # type: string; example: {{.GRPCPayload}} .GRPCService # type: string; example: {{.GRPCService}} .GRPCMethod # type: string; example: {{.GRPCMethod}}
.KafkaTopic # type: string; example: {{.KafkaTopic}} .KafkaPayload # type: string; example: {{.KafkaPayload}}
.AMQPExchange # type: string; example: {{.AMQPExchange}} .AMQPRoutingKey # type: string; example: {{.AMQPRoutingKey}} .AMQPQueue # type: string; example: {{.AMQPQueue}} .AMQPPayload # type: string; example: {{.AMQPPayload}} ```
{{ expr }}. We recommend pipeline format (|) of the functions.```bash # Supported functions defined in ./template_helper.go
-
- jsonPath # doc: https://github.com/antchfx/xpath
- gJsonPath # doc: https://github.com/tidwall/gjson
- xmlPath # doc: https://github.com/antchfx/xpath
- uuidv5 # uuid v5 sha1 hash
- redisDo # run redis commands. For example {{redisDo "RPUSH" "arr" "hi"}}
- ...
# Supported functions inherited from # https://github.com/Masterminds/sprig/blob/master/functions.go
- replace
- uuidv4
- regexMatch
- ...
# Examples {{.HTTPHeader.Get "X-Token" | eq "t1234"}} {{.HTTPBody | jsonPath "user/first_name" | replace "A" "a" | uuidv5 }} {{.HTTPBody | gJsonPath "users.0.first_name" }} {{.HTTPBody | xmlPath "node1/node2/node3"}} ```
Openmock also by default provides an API on port 9998 to control the running instance. See api documentation. You can serve the api documentation by getting go-swagger and running:
./swagger serve --host 0.0.0.0 --port 9997 docs/api_docs/bundle.yaml"
Openmock has a command-line interface to help with certain tasks interacting with openmock instances. This is
invoked with the omctl command. This uses the cobra library to provide a discoverable CLI; run omctl for a list of commands / flags.
Pushes a local openmock model from the file system to a remote instance.
# Adds templates from the ./demo_templates directory to the instance running on localhost.
omctl push --directory ./demo_templates --url http://localhost:9998
# demo_templates/http.yaml
# $ curl 0:9999/ping
# OK
- key: ping
kind: Behavior
expect:
http:
method: GET
path: /ping
actions:
- reply_http:
status_code: 200
body: OK
headers:
Content-Type: text/html
# $ curl 0:9999/token -H X-Token:t1234 -H Y-Token:t1234
# OK
- key: header-token-200
kind: Behavior
expect:
condition: '{{.HTTPHeader.Get "X-Token" | eq "t1234" | and (.HTTPHeader.Get "Y-Token" | eq "t1234")}}'
http:
method: GET
path: /token
actions:
- reply_http:
status_code: 200
body: OK
# $ curl 0:9999/token
# Invalid X-Token
- key: header-token-401
kind: Behavior
expect:
condition: '{{.HTTPHeader.Get "X-Token" | ne "t1234"}}'
http:
method: GET
path: /token
actions:
- reply_http:
status_code: 401
body: Invalid X-Token
- key: get-pdf
expect:
http:
method: GET
path: /api/v1/:ClientID/pdf
condition: '{{
(.HTTPHeader.Get "Authorization" | contains "exp") | and
(.HTTPHeader.Get "x-timestamp" | eq "" | not)
}}'
actions:
- reply_http:
status_code: 200
headers:
Content-Type: application/pdf
body_from_binary_file: ./data/example.pdf
binary_file_name: example_pdf.pdf # optional file name
- key: get-json
expect:
http:
method: GET
path: /api/v1/:ClientID/json
condition: '{{
(.HTTPHeader.Get "Authorization" | contains "exp") | and
(.HTTPHeader.Get "x-timestamp" | eq "" | not)
}}'
actions:
- reply_http:
status_code: 200
headers:
Content-Type: application/json
body_from_file: ./data/example.json # only text files supported
# demo_templates/grpc.yaml
- key: example_grpc
expect:
grpc:
service: demo_protobuf.ExampleService
method: ExampleMethod
actions:
- reply_grpc:
payload_from_file: './files/example_grpc_response.json'
# demo_templates/kafka.yaml
- key: test_kafka_1
kind: Behavior
expect:
kafka:
topic: hello_kafka_in
actions:
- publish_kafka:
topic: hello_kafka_out
payload: >
{
"kafka": "OK",
"data": {}
}
- key: test_kafka_2
kind: Behavior
expect:
kafka:
topic: hello_kafka_in_2
actions:
- publish_kafka:
topic: hello_kafka_out
payload_from_file: './files/colors.json' # the path is relative to OPENMOCK_TEMPLATES_DIR
If you started the example from docker-compose, you can test the above kafka mocks by using a kt docker container.
# Exec into the container
docker-compose exec kt bash
# Run some kt commands inside the container
# Notice that the container is within the docker-compose network, and it connects to "kafka:9092"
$ kt topic
$ echo '{"123":"hi"}' | kt produce -topic hello_kafka_in -literal
$ kt consume -topic hello_kafka_out -offsets all=newest:newest
```yaml
key: test_amqp_1 kind: Behavior expect: amqp: exchange: exchange_1 routing_key: key_in queue: key_in actions:
key: test_amqp_2 kind: Behavior expect: amqp: exchange: exchange_1
$ claude mcp add openmock \
-- python -m otcore.mcp_server <graph>