bouchon
Efficient API mocking with cool libraries.
Summary
- Big picture
- Before using bouchon
- Quick start
- Advanced usage
- Bouchon full API
- Use bouchon for integration testing
- Installation
- Other related packages
- License
Big picture
Bouchon provides a way to make mocks easily with redux and reselect.
Redux keeps your API stateful in order to create / edit / delete objects in a fake API and reselect allows to retrieve any data from that state.
You define some data in a JSON file and your actions / reducers / selectors / middlewares / routes in a JS file. These two files is what I call a fixture.
Each route (verb + url) defines an action, a selector and some optional middlewares.
Before using bouchon
It is advisable to be comfortable with the following softs and techniques for using bouchon:
Quick start
Follow the following instructions to make your first fixture. The full code is available in the repository.
Start an empty project, install bouchon and create folders and files:
$ mkdir bouchon-tutorial && cd $_
$ npm init
$ npm install bouchon babel-core babel-polyfill babel-preset-es2015 --save-dev
$ mkdir article && cd $_ && touch data.json && touch index.js
Note: Examples are written in ES2015 and therefore, you have to install and configure Babel too. See the Babel section for more informations.
JSON data
Each fixture defines a JSON files with some data that will be saved in the state:
// article/data.json
:
[
{
"id": 1,
"title": "title 1",
"body": "body 1"
},
{
"id": 2,
"title": "title 2",
"body": "body 2"
}
]
Actions
In order to limit the boilerplate, bouchon uses redux-act that provides a simpler API to create actions and reducers. No types and switch cases are required.
Start by creating some actions:
// article/index.js ; const actions = get: post: delete: ;
Selectors
Continue by writing some selectors. Selectors uses reselect and are used to select data from the state.
// article/index.js ; // [...] const selectors = {};selectors statearticles;selectors ;
- The first curried function takes a merge of query, params and body parameters.
- The second takes the full
state
allowing to select any part of data.
Reducers
Like every redux apps, you have to implement reducers that will maintain the state according to dispatched actions.
Here a basic implementation for the GET, POST, DELETE actions (reminder, bouchon is using redux-act and the syntax is different that one you may know):
// article/index.js // [...] const reducer = actionsget: state actionspost: ...state body actionsdelete: { const copy = state; // be careful to never mutate the state return copy; };
Each function of the reducer takes the state
the current fixture (not the full state)
and an object with req
, res
, query
parameters, params
parameters, body
parameters
and must return a new state (be careful to NEVER mutate the state).
Routes
Now, we have to define the routes of our API.
// article/index.js // [...] const routes: 'GET /': action: actions.get selector: selectors.all status: 200 'GET /:id': action: actions.get selector: selectors.byId status: 200 'POST /:id': action: actions.post selector: selectors.byId status: 201 'DELETE /:id': action: actions.delete status: 204 ;
Summary
Add a name
that will be used as the key in the state where your data
live and you're done.
See the full fixture:
// article/index.js ; const actions = get: post: delete: ; const reducer = actionsget: state actionspost: ...state paramsbody actionsdelete: { const copy = state; // be careful to never mutate the state return copy; }; const selectors = {};selectors statearticles;selectors ; const routes = 'GET /': action: actionsget selector: selectorsall status: 200 'GET /:id': action: actionsget selector: selectorsbyId status: 200 'POST /:id': action: actionspost selector: selectorsbyId status: 201 'DELETE /:id': action: actionsdelete status: 204 ; name: 'articles' data: reducer: reducer endpoint: 'articles' routes: routes;
Start bouchon
Bouchon is looking for files with .fixture.js
suffix. Since your fixtures are
written in ES2015, you have to workaround a little bit in order to be able to load
them.
The tweak is just to create a unique file that requires all your fixtures. Just place Babel at the top and you're done.
// all.fixture.js ;; moduleexports = default: default ;
Note: Don't forget to create your
.babelrc
file. See the Babel section for more informations.
You can now start bouchon by providing your fixture folder and an optional port:
./node_modules/.bin/bouchon -d . -p 3000
You should see bouchon registering your urls:
00:32:21.938Z INFO server: Registering "GET /articles/"
00:32:21.941Z INFO server: Registering "GET /articles/:id"
00:32:21.942Z INFO server: Registering "POST /articles/:id"
00:32:21.942Z INFO server: Registering "DELETE /articles/:id"
00:32:22.001Z INFO server: App listening at port 3000.
Do a GET to retrieve articles:
$ curl http://localhost:3000/articles | python -mjson.tool[ , ]
Do a DELETE to remove an article:
$ curl -X DELETE http://localhost:5556/articles/1$ curl http://localhost:3000/articles | python -mjson.tool[ ]
Hey, it works!
Advanced usage
Now that you have an idea of how bouchon works, let's continue with complex workflows.
Export actions
You should split your data and fixture by API namespace.
For example, if you have ./api/articles
and ./api/operations
, make two fixtures
that each handles their data.
But if you need to alter the state of an another fixture, you can export your actions and reuse them in any fixture.
Export selectors
In the same way, you can export selectors.
Imagine that you have 2 fixtures like articles and authors. If you want to join the data of both in a single request, you can do something like that:
// authors/index.js const selectors = {}selectors stateauthors; name: 'authors' data: ; // [...] // articles/index.js ;; const selectors = {};selectors statearticles; selectors ; const routes = 'GET /': selector: selectorsallWithAuthors status: 200 ; name: 'articles' data: routes: routes;
Multiple actions
Because your data is splitted into several fixtures, you may need to alter the state of several parts of your state for a single request.
For achieve that, you can use several actions for a route. Each action will be dispatched and will update the state according to your reducers.
const routes = 'POST /': action: articlesActionscreate operationsActionssetToDone selector: selectorsbyId status: 200 ;
Backend actions
In some use cases, the workflow can be asynchronous. For example, an API request can just respond "OK" and save an object in a queue processed later by a backend process.
Bouchon provides a feature named backendAction
that allows to dispatch an action
in the future in order to simulate a backend process.
See how to return an object identifier in the request's response and create the object five seconds later.
const routes = 'POST /': action: operationsActionscreate backendAction: action: actionscreate operationsActionssetToDone delay: 5000 selector: operationsSelectorslastId status: 201 ;
Asynchronous sample
To illustrate all the previous options, imagine that workflow:
- When I POST a request on
./articles
, it returns the detail of an operation that contains the payload data and that will be handled later by an unknown process, - when the operation has been processed, my article is created in the database with the payload data of my operation,
- the operation is flagged as 'DONE',
- I can do a GET on
./articles
to list all created articles.
See how you can export / import actions and selectors and how to define your route
with backendActions
in this sample.
Delays
Bouchon is useful to test your app in several conditions.
If you want to simulate a slow API, just add a delay in your action
:
const routes = 'GET /': action: action: operationsActionscreate delay: 2000 selector: selectorsall status: 200 ;
delay
can also be an array like [1000, 5000]
to simulate a delay between 1000 and 5000 milliseconds.
Middlewares
You certainly know how Express middlewares work? bouchon supports middlewares in the same way.
For example, if you want a complete solution for pagination, you can easily write a middleware like this:
// data is the selected data from your selectorconst setPaginationHeaders = { const page = reqquerypage || 1; const perPage = reqqueryperPage || 10; const pageCount = Math; const totalCount = datalength; const slicedData = data; const headers = 'x-page': page 'x-per-page': perPage 'x-page-count': pageCount 'x-total-count': totalCount ; // if data are set in the response object, bouchon will return that data instead // of those selected by your selector resdata = slicedData; // set pagination headers res; // do not forget to call next to continue the chain ;};
Then just declare it in your route:
const routes = 'GET /': action: actionsget selector: selectorsall middlewares: setPaginationHeaders status: 200 ;
Combine fixtures
If you are already familiar with redux,
you certainly know how combineReducers
work.
Bouchon is providing similar helpers to combine reducers and routes and thus allow
to split your fixtures and data by namespace by using the endpoint
key.
It will combine the reducers and the routes.
; ;; name: 'library' endpoint: 'library' ...;
Use Babel for your fixtures
If you want to write your fixtures with modern Javascript, you have to workaround a little bit in order to require Babel before your fixtures.
The simplest way is a have only one fixture that requires all the others.
Note: Don't forget to create a
.babelrc
at the root of your fixtures with the ES2015 preset:
{
"presets": [ "es2015" ]
}
Hot reload
Bouchon is providing a basic hot reload feature that allows to retrieve a previous state when the process is restarting. It's useful when you just want to test a different status code / output in a route without losing all the context of your mocks.
To activate the feature, add the --hot
option to your command line. When a query is performed, a .bouchonHotReload
file
will be written with the full state. Bouchon will read this file when being restarted by a process supervisor like nodemon,
allowing to retrieve all the context of your mocks.
When the process is exited with ctrl+c
, the file is removed.
Tips: To start Bouchon with nodemon, you can do something like this:
npm run start:dev -- -- -d /path/to/your/fixtures --hot
Bouchon full API
TODO
Use bouchon for integration testing
Bouchon is providing an API useful for integration tests.
For example, to test an app in a browser, you can start bouchon at the beginning of the test, execute your test with a Selenium based tool and stop bouchon at the end.
Bonus: bouchon is recording all actions done during the test so you can check that your process did exactly what you are expected at the end of your test.
;;;;; const expect = chaiexpect; ;
Installation
npm install --save bouchon
Other related packages
- bouchon-toolbox: a set of useful reducers, selectors and middlewares for common use cases
- bouchon-samples: some examples for inspiration
License
MIT.