{"id":19884275,"url":"https://github.com/yawaramin/stubbex","last_synced_at":"2025-07-25T22:11:40.340Z","repository":{"id":49394737,"uuid":"153963226","full_name":"yawaramin/stubbex","owner":"yawaramin","description":"Stub and validate HTTP endpoints with ease.","archived":false,"fork":false,"pushed_at":"2018-11-17T21:41:39.000Z","size":260,"stargazers_count":11,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-24T23:04:19.400Z","etag":null,"topics":["contract-testing","elixir","erlang","http","service-virtualization","stub-server","stubbing"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yawaramin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-10-21T01:30:06.000Z","updated_at":"2024-05-30T22:48:01.000Z","dependencies_parsed_at":"2022-09-13T22:53:07.324Z","dependency_job_id":null,"html_url":"https://github.com/yawaramin/stubbex","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yawaramin%2Fstubbex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yawaramin%2Fstubbex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yawaramin%2Fstubbex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yawaramin%2Fstubbex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yawaramin","download_url":"https://codeload.github.com/yawaramin/stubbex/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252062716,"owners_count":21688588,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["contract-testing","elixir","erlang","http","service-virtualization","stub-server","stubbing"],"created_at":"2024-11-12T17:26:03.317Z","updated_at":"2025-05-02T15:30:51.556Z","avatar_url":"https://github.com/yawaramin.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Stubbex–stub and validate with ease\n\nThis is a stub server, like Mountebank or Wiremock. Its purpose is to\nautomatically save responses from real endpoints and use those going\nforward whenever you try to hit the stub endpoint. It can also\ninterpolate responses from template stubs that you control, and validate\nsaved stubs against the real responses.\n\nIn other words, Stubbex sets up what Martin Fowler calls a\n[self-initializing fake](https://martinfowler.com/bliki/SelfInitializingFake.html).\n\n## Guide\n\n* [Emphasis on Simplicity](#emphasis-on-simplicity)\n* [Concurrency](#concurrency)\n* [Request Precision](#request-precision)\n* [Installation](#installation)\n  * [Configuration](#configuration)\n* [Example](#example)\n* [The Hash](#the-hash)\n  * [The Stubbex Cookie and Scenarios](#the-stubbex-cookie-and-scenarios)\n* [Developer Workflow](#developer-workflow)\n  * [Editing Existing Stubs](#editing-existing-stubs)\n  * [Stubbing Non-Existent Endpoints](#stubbing-non-existent-endpoints)\n* [Templating the Response](#templating-the-response)\n  * [Injecting JSON into Responses](#injecting-json-into-responses)\n* [Validating the Stubs](#validating-the-stubs)\n  * [JSON Schema Validation](#json-schema-validation)\n* [Limitations](#limitations)\n\nWhat sets Stubbex apart (in my opinion) are three things:\n\n## Emphasis on Simplicity\n\nThe Stubbex philosophy is to do everything with as little configuration\nas possible–typically zero config. Every stub server I've come across\nrequires a configuration file, or some HTTP commands, or a unit-test\nframework, to tell it what to do.\n\nStubbex requires no configuration and tries to 'do the right thing':\ncall out to the real endpoints only if it needs to, and replay existing\nstubs whenever it can. It can do validation of large subsets of stubs\nwith a single command.\n\nIf you want to set up stubs manually, you have to place the stub files\nin the format that Stubbex expects, at the right location, as explained\nbelow. However, you can also take advantage of Stubbex's initial\nrecording ability to edit already-existing stub files in place–even for\nservices that haven't been written yet.\n\n## Concurrency\n\nStubbex is designed to be massively concurrent. It takes advantage of\nElixir, Phoenix Framework, and the Erlang system to handle concurrent\nincoming requests efficiently. Note that, since this project is new,\nthis has not been tested yet and there are no benchmarks. But _in\ntheory,_ you should be able to start up a single Stubbex server and hit\nit from many different tests and CI builds. It will automatically fetch,\nsave, and reply with responses.\n\nRelated to concurrency, another huge benefit that Stubbex brings to the\ntable (thanks to its implementation stack) is fault-tolerance. You can\nsend it bad inputs in a few different ways–and I discuss some of them in\nthe sections below–but what they all have in common is that, short of a\ntruly unforeseen catastrophic failure, Stubbex will recover from every\nerror and immediately be ready to handle the next request.\n\n## Request Precision\n\nThis means that Stubbex stores and responds to requests using _all\npertinent_ information contained in the requests, like the method (GET,\nPOST, etc.), URLs, query parameters, request headers if any, and request\nbody if any. You can get it to save and give you a response with\ncomplete precision. So you can stub any number of different hosts,\nendpoints, and specific requests.\n\n## Installation\n\nYou can either compile and run Stubbex using a local installation of\nElixir, or download the latest pre-built release tarball from the\nreleases page: https://github.com/yawaramin/stubbex/releases . For\nexample, say you download `stubbex-N.N.N-osx.tar.gz` and unpack it:\n\n```\n~/src $ mkdir stubbex; cd stubbex\n~/src/stubbex $ tar xzf stubbex-N.N.N-osx.tar.gz\n```\n\n**Note:** for the moment I'm uploading the binary for only the latest\nrelease, and only for macOS. The plan is to do releases for other OSs at\nsome point!\n\n### Configuration\n\nStubbex has certain configurable options which it reads at startup from\nthe system environment. These are:\n\n- `PORT`: mandatory port number, Stubbex will refuse to start up without\n  it\n- `stubbex_cert_pem`: optional path to a root HTTPS certificate (may be\n  needed for making HTTPS requests), default is `/etc/ssl/cert.pem`\n- `stubbex_stubs_dir`: optional path where Stubbex should keep the\n  `stubs` directory, default is `.`\n- `stubbex_timeout_ms`: optional numeric value of how long Stubbex\n  should wait for requests and responses, in milliseconds. Default is 10\n  minutes.\n- `stubbex_offline`: optional boolean value of whether Stubbex should\n  record new stubs or not, default is `false`. If `true`, any stub call\n  that ends up needing to record a new stub, will fail and show client-\n  side as an Internal Server Error.\n\n## Example\n\nSuppose you want to stub the response from a JSON Placeholder URL,\nhttps://jsonplaceholder.typicode.com/todos/1 . First, you start up\nStubbex:\n\n```\n~/src/stubbex $ PORT=4000 ./bin/stubbex foreground\n```\n\nThen, send it a request:\n\n```\n~/src $ curl localhost:4000/stubs/https/jsonplaceholder.typicode.com/todos/1\n{\n  \"userId\": 1,\n  \"id\": 1,\n  \"title\": \"delectus aut autem\",\n  \"completed\": false\n}\n```\n\nNotice the completely mechanical translation from the real URL to the\nstub URL. You can probably guess how it works:\n\n* Prefix with `localhost:4000/stubs/`\n* Remove the `:/`\n* That's it, you now have the stub URL. This makes it pretty easy to\n  configure real and test/QA/etc. endpoints.\n\nNow, check the `~/src/stubbex/stubs` subdirectory. There's a new\ndirectory structure and a stub file there. Take a look:\n\n```\n~/src/stubbex $ less stubs/https/jsonplaceholder.typicode.com/todos/1/A56255E8FEE7CC38479F0862D6921C04.json\n{\n  \"url\": \"https://jsonplaceholder.typicode.com/todos/1\",\n  \"response\": {\n    \"status_code\": 200,\n    \"headers\": {...\n```\n\nThe stub is stored in a predictable location\n(`stubs/protocol/host/path.../hash.json`) and is pretty-printed for your\nviewing pleasure.\n\n## The Hash\n\nNotice the file name of the stub, `A56255E8....json`. That's an MD5-\nencoded hash of the request details:\n\n* Method (GET, POST, etc.)\n* URL\n* Query parameters\n* Headers\n* Body\n\nThese five details uniquely identify any request, to any endpoint.\nStubbex uses this hash to look up the correct response for any request,\nand if it doesn't have it, it will fetch it and save it for next time.\n\nThis 'request-addressable' file name allows Stubbex to pick the correct\nresponse stub for any call without having to open and parse the stub\nfile itself. It effectively uses the filesystem as an index data\nstructure.\n\n### The Stubbex Cookie and Scenarios\n\nAn implicit assumption here (indeed, Stubbex's basic assumption) is that\neach _unique_ request has _exactly one_ response. So for example, a `GET\n/cart` request should _always_ return the exact same response, for\nexample `{}`. But then what if the user adds an item to their cart? Most\nreal-world servers use some session-management mechanism, like a cookie,\nto track the user's current state. Stubbex does the same thing; it sets\na `stubbex` cookie in every response that is exactly equal to the hash\nof the request parameters.\n\nAnd, if your app respects the `Set-Cookie` header and sends servers the\ncookies they set (including the `stubbex` cookie), you can establish an\naudit trail between every request and response. Here's an example of how\nit would work:\n\n```\nClient: log in\nStubbex: response with cookie1 generated from log in request\nC: get cart with cookie1 (i.e. a `Cookie: stubbex=cookie1` header\n   because the client respects the server cookies)\nS: response with cookie2 generated from get cart request with cookie1\nC: add item to cart with cookie2\nS: response with cookie3 generated from add item request with cookie2\nC: get cart with cookie3\n...\n```\n\nEffectively, you have a scenario (or a session) established by a chain\nof `stubbex` cookies. No config and no special commands; just the\nidiomatic HTTP state management mechanism. And, because Stubbex is an\nimmutable (well, at least in the same way that git is) store of\nrequest-response pairs, you will deterministically get the exact same\nresponse for every request with the right cookies–even for otherwise\nidentical requests like `GET /cart`.\n\n## Developer Workflow\n\nTo use Stubbex as part of your dev workflow, first you'll need a running\nStubbex instance. The easiest way to get it running is as shown above.\nAlternatively, you might\n[deploy](https://hexdocs.pm/distillery/guides/systemd.html#run-app-in-foreground-using-a-simple-systemd-configuration)\nStubbex to a shared internal server (**WARNING:** by no means expose it\nto the outside world!) and use that for development and testing across\nmultiple developer machines and CI builds.\n\nNext, set up a QA/test config in your app that points all the base URLs\nfor every service call to Stubbex, e.g.\n`http://localhost:4000/stubs/http/...`. You would use your development\nstack's normal configuration management system here. If you have a\nserious networked app, you likely already have separate endpoints\nconfigured for QA and PROD. In this case you'd just switch the QA\nendpoints to the stubbed versions, as shown above.\n\nThen, run your app with this QA config and let Stubbex automatically\ncapture and replay the stubs for you. The stubs will be available both\nduring iterative development and test suite runs as long as they use the\nsame QA config.\n\n### Editing Existing Stubs\n\nStubbex caches all non-templated (i.e. static) stubs in memory for a\nperiod of time (by default, ten minutes) to serve the response as fast\nas possible. But you might like to edit an existing stub and immediately\nsee the changed response. So, Stubbex will automatically clear its cache\nfor a stub when you edit (or delete) that stub. This helps with\niterative development.\n\nNote that on Linux and the BSDs you'll need to install `inotify-tools`\nto make instant edits work. See\nhttps://hexdocs.pm/file_system/readme.html for more details.\n\n### Stubbing Non-Existent Endpoints\n\nSometimes you'll need to stub out responses from endpoints that haven't\nactually been written yet. Manually naming and placing the stub files in\nthe right directories would be a pain. Fortunately, Stubbex\nautomatically generates stub files for you _even for endpoints that\ndon't exist._ For example, you can send the following request:\n\n```\ncurl localhost:4000/stubs/http/bla\n```\n\nStubbex will try to get the response, see that it can't, and put a stub\nfile with the right name, in the right place, _with a 501 (not\nimplemented) status_ and an empty body:\n\n```\n~/src/stubbex $ less stubs/http/bla/FC4443CF188F5039AB8C6C96FC500EB9.json\n{\n  \"url\": \"http://bla\",\n  \"response\": {\n    \"status_code\": 501,\n    \"headers\": {},\n    \"body\": \"\"\n  },...\n```\n\nYou can edit this stub, put in whatever response you need, and keep\ngoing.\n\n**WARNING:** don't use Postman or other browser-based tools to make\nrequests to Stubbex for the purpose of setting up stubs for later use.\nThey may add additional headers beyond your control, and Stubbex's\nresponse matching is, as mentioned above, sensitive to exact request\nheaders. For example, see\nhttps://github.com/postmanlabs/postman-app-support/issues/443 (a\nfive-year old issue wherein Postman sends additional headers in all\nrequests). If you want to set up stubs beforehand, you can:\n\n* Hit Stubbex from your app (this is best)\n* Use a tool like `curl` which sends requests exactly as you specify\n* Write the stub files by hand (way less fun).\n\n## Templating the Response\n\nYou can template response stub files and Stubbex will immediately pick\nup changes to the stubs and start serving on-the-fly evaluated\nresponses. Templates are named like `hash.json.eex` (they are [Embedded\nElixir](https://hexdocs.pm/eex/EEx.html#module-tags) files) and can\ncontain any valid Elixir language expression as well as refer to request\nparameters. If you have a template like\n`stubs/https/jsonplaceholder.typicode.com/todos/1/E406D55E4DBB26C8050FCDC3D20B7CAA.json.eex`,\nyou can edit it with your favourite text editor and insert valid markup\naccording to the rules of EEx. For example, the above stub by default\nhas a body like this:\n\n```\n\"body\": \"{\\n  \\\"userId\\\": 1,\\n  \\\"id\\\": 1,\\n  \\\"title\\\": \\\"delectus aut autem\\\",\\n  \\\"completed\\\": false\\n}\"\n```\n\nYou can set the todo to be automatically completed if we're past 2017:\n\n```\n\"body\": \"{\\n  \\\"userId\\\": 1,\\n  \\\"id\\\": 1,\\n  \\\"title\\\": \\\"delectus aut autem\\\",\\n  \\\"completed\\\": \u003c%= DateTime.utc_now().year \u003e 2017 %\u003e\\n}\"\n```\n\nOr you can use the user-agent header as part of the todo title:\n\n```\n\"body\": \"{\\n  \\\"userId\\\": 1,\\n  \\\"id\\\": 1,\\n  \\\"title\\\": \\\"User agent: \u003c%= headers |\u003e Stubbex.header_values(\"user-agent\") |\u003e List.first %\u003e\\\",\\n  \\\"completed\\\": false\\n}\"\n```\n\nThen if you get the response again (with the `curl` command in\n[Example](#example)), you'll see that the `completed` attribute is set\nto `true` (assuming your year is past 2017); or that the todo title is\n`User agent: curl/7.54.0` (e.g.), or any other result, depending on\nwhich markup you put in place.\n\nRequest parameters are available under the following names:\n\n* `url`: string\n* `query_string`: string\n* `method`: string\n* `headers`: list of pairs of string keys (header names) and string\n  values; you can get values with\n  `Stubbex.header_values(\"user-agent\")` (all lowercase) syntax. Note\n  that this will return a list of header values (because HTTP headers\n  may be duplicated), and you'll probably want to get the first value\n  with `List.first(...)` as shown above\n* `body`: string\n\nThere are many other useful data manipulation functions in the [Elixir\nstandard library](https://hexdocs.pm/elixir/api-reference.html#content),\nwhich can all be used as part of the EEx templates. This is of course in\naddition to all the normal features you'd expect from a language, like\narithmetic, looping and branching logic, etc. I recommend taking a look\nat the Embedded Elixir link above; it has a five-minute crash course on\nthe template markup.\n\nYou may be thinking, how to get a stub in the first place, to start\nediting? Simple! Let Stubbex record it for you by first hitting a real\n(or fake!) endpoint. Then add the `.eex` file extension to the stub JSON\nfile and insert whatever markup you need.\n\nNote that Stubbex doesn't cache template stub responses, because these\nmight change dynamically with every request (e.g., you might inject the\ncurrent time into the response).\n\n### Injecting JSON into Responses\n\nBe careful with putting markup, especially JSON, in stubs. The templated\nstub is passed through an interpolation engine (EEx), then decoded from\na JSON-encoded string into an Elixir-native data structure. If for\nexample you miss escaping the template stub's body JSON properly, you'll\nget runtime errors from Stubbex that look like this:\n\n```\n[error] GenServer \"/stubs/https/jsonplaceholder.typicode.com/todos/1\" terminating\n** (Poison.SyntaxError) Unexpected token at position 1008: h\n...\n```\n\n(`Poison` is the JSON decoder module).\n\nIn this case I forgot to escape the double-quotes around the body JSON\nattributes, and Stubbex misinterpreted the result.\n\nTo safely escape JSON-encoded strings in responses, you can use the\n`Stubbex.stringify` function in a template tag:\n\n```\n...\n\"body\": \"\u003c%= Stubbex.stringify(\"\"\"\n  {\n    \"userId\": 1,\n    \"id\": 1,\n    \"title\": \"Do the thing\",\n    \"completed\": false\n  }\n  \"\"\") %\u003e\"\n...\n```\n\nThe basic structure here is: `\"body\": \"\u003c%= (Elixir string expression) %\u003e\"`,\nand the `(Elixir string expression)` is injected into the final response\nby the templating engine. The `Stubbex.stringify` is a normal Elixir\nfunction call, and the triple double-quotes are used to (a) do the\ninitial escaping of the double-quotes in the JSON body, and (b) get rid\nof leading whitespace (in fact all leading whitespace from every line in\nthe triple-quoted string _to the left of the closing triple-quote_ will\nbe removed).\n\n## Validating the Stubs\n\nThe trouble with static stubs is that they will get out of date. To\nguard against this happening, one option is to make 'someone'\nresponsible for keeping the stub files up-to-date. Under [contract\ntesting](https://martinfowler.com/bliki/ContractTest.html), you might\nactually also delegate some of the responsibility for stub upkeep for\neach service's stubs to the corresponding service provider (obviously,\nthis only works if you can reach an agreement with the service\nprovider).\n\nAt the bare minimum, you would zip up each provider's stubs periodically\nand 'throw it over the wall' and let them figure out if they're still\nconforming to the request-response expectations. But this can be a tough\nsell, so Stubbex provides a convenience to _validate_ stubs. For\nexample, to validate the stubs for the 'JSON Placeholder Todo ID 1'\nendpoint we use above, you can send the following request:\n\n```\n~/src/stubbex $ curl localhost:4000/validations/https/jsonplaceholder.typicode.com/todos/1\n```\n\nAnd Stubbex replies with a colorized diff suitable for display in a\nterminal:\n\n\u003cimg width=\"1364\" alt=\"Stubbex validation output\" src=\"https://user-images.githubusercontent.com/6997/47684295-c28fe900-dba8-11e8-8f7f-3699d18a0111.png\"\u003e\n\nTo validate _all_ the JSON Placeholder _todos,_ you can send:\n\n```\n~/src/stubbex $ curl localhost:4000/validations/https/jsonplaceholder.typicode.com/todos/\n```\n\nTo validate _all_ the JSON Placeholder _stubs,_ you can send:\n\n```\n~/src/stubbex $ curl localhost:4000/validations/https/jsonplaceholder.typicode.com/\n```\n\nHowever, Stubbex doesn't support validating stubs at any higher level\nand will error if you try. I think this is a reasonable balance if\nyou're trying to delegate validating stubs to service providers. They\nwould just worry about their own stubs.\n\n*Tip:* when validating long responses, it's helpful to pipe the output\ninto `less -R`, because it can understand and show colours:\n\n```\n~/src/stubbex $ curl localhost:4000/validations/... | less -R\n```\n\n### JSON Schema Validation\n\nSometimes it isn't practical to validate the entire response body,\nbecause a real server response might differ greatly between requests. In\nthese cases it's still valuable to know whether the _shape_ of the\nresponse matches what you expect.\n\nStubbex allows you to validate the shape of the response by specifying\nits [JSON Schema](http://json-schema.org/) in your stub. The workflow\nwould look very similar to the other Stubbex workflows: start by sending\na normal stub request from your app (which you may already have done),\nthen rename the `stubs/path/to/HASH.json` file to\n`stubs/path/to/HASH.json.schema`. This will tell Stubbex to use JSON\nschema validation for this stub. Then, put the response's expected JSON\nSchema object in the stub's `response.body` field.\n\nFor example, here's a schema for the todos we show above:\n\n```\n{\n  \"url\": \"https://jsonplaceholder.typicode.com/todos/1\",\n  \"response\": {\n    ...,\n    \"body\": {\n      \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n      \"title\": \"Todo\",\n      \"description\": \"A reminder.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"userId\": {\"type\": \"integer\"},\n        \"id\": {\"type\": \"integer\"},\n        \"title\": {\"type\": \"string\"},\n        \"completed\": {\"type\": \"boolean\"}\n      },\n      \"required\": [\"userId\", \"id\", \"title\", \"completed\"]\n    }\n  },\n  ...\n}\n```\n\n**Note:** due to the specific schema validation library that Stubbex\nuses, the schemas must be versioned at Draft 4 at most.\n\nFinally, to do an actual validation, run the usual validation command:\n\n```\n~/src/stubbex $ curl localhost:4000/validations/https/jsonplaceholder.typicode.com/todos/1\n```\n\n\u003cimg width=\"1383\" alt=\"screen shot 2018-11-02 at 22 21 07\" src=\"https://user-images.githubusercontent.com/6997/47947187-0f97f600-deee-11e8-9fe3-a84a4e59542b.png\"\u003e\n\nThe response body is a green `:ok` to indicate that the schema\nvalidation succeded.\n\nNow, to simulate a validation error, try changing the `completed`\nattribute type to `string`, and rerun the validation:\n\n\u003cimg width=\"1384\" alt=\"screen shot 2018-11-02 at 22 22 04\" src=\"https://user-images.githubusercontent.com/6997/47947189-1e7ea880-deee-11e8-8bc8-697f1830b3c0.png\"\u003e\n\nThe response body is a red description of the error and the path to the\nerroring attribute.\n\n## Limitations\n\n* Not enough tests right now (run with\n  `stubbex_stubs_dir=test mix test.watch --stale` for continuous\n  iterate-and-run cycle)\n* No benchmarks right now\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyawaramin%2Fstubbex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyawaramin%2Fstubbex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyawaramin%2Fstubbex/lists"}