This is a CLI tool which can be used to run api-benchmark tests. Its interface is inspired by mocha.
The idea behind this tool is to allow you to define tests in one or across multiple scripts using easy to use hooks much like mocha
.
'use strict';
suite('Status Route', () => {
service('my-service', 'http://localhost:8080');
suite('Multiple requests in Parallel', () => {
options({
runMode: 'parallel',
minSamples: 200,
maxTime: 20,
});
route('status', {
method: 'get',
route: 'status',
expectedStatusCode: 200,
maxMean: 0.2, // 200ms
});
});
suite('Multiple requests in Sequence', () => {
options({
runMode: 'sequence',
minSamples: 200,
maxTime: 20,
});
route('status', {
method: 'get',
route: 'status',
expectedStatusCode: 200,
maxMean: 0.2, // 200ms
});
});
});
In the above example we use the suite
hook to define a test suite. This suite will result in a single api-benchmark
test run. The suite
hook takes a name (just for the output) and a callback function that will be executed to configure the suite. The callback will include the other hooks which lets you define the service, routes, and options.
You can have multiple test files which api-bench-runner
will aggregate together allowing you to keep you test code clear and module.
By convention api-bench-runner
will look for tests under the bench
folder.
├── bench
│ ├── nested-test-example.js
│ └── simple-test-example.js
└── lib
└── server.js
> node_modules/.bin/api-bench-runner .
This command will look for all the tests under the bench
folder and run them displaying the results in the terminal.
You can redirect api-bench-runner
to a different location if you choose not to put your tests under a bench
folder.
> node_modules/.bin/api-bench-runner ./benchmark
You can use glob patterns to find test files as well
> node_modules/.bin/api-bench-runner ./test/**/*.bench.js
The output of the tool will be a print out to the terminal showing the test results. When all tests pass (or you did not define restrictions which would cause failures) you will see each test (grouped by test suite) listing info such as operations per second and a summary saying everything passed.
If however you do have restrictions and the route did not meet them or something else went wrong you will see that test run listed with an error and a summary of all test failures printed out at the bottom.
You can specify different reporters for api-bench-runner
to use depending on what kind of output you need. To pick a reporter use the --reporter
flag.
By default it will print to the terminal but you can make this explicit by setting the --reporter
flag to stdterm
or default
> node_modules/.bin/api-bench-runner ./test/**/*.bench.js --reporter default
You can have api-bench-runner
generate an HTML report. The report generated is the report generated by api-benchmark. To pick the HTML reporter set the --reporter
flag to html
.
> node_modules/.bin/api-bench-runner ./test/**/*.bench.js --reporter html
By default the HTML report will be generated in the working directory that api-bench-runner
is in and will save it to the file benchmarks.js
. To specify a different file name you can use the --out
flag.
> node_modules/.bin/api-bench-runner ./test/**/*.bench.js --reporter html --out my-results.html
Note The --out
flag has no effect on stdterm
reporter and right now is only used by the html
reporter.
If you would like to see the results in the terminal and get a HTML report generated you can specify both reporters via the --reporter
flag.
> node_modules/.bin/api-bench-runner ./test/**/*.bench.js --reporter stdterm,html
You can define a test suite via the suite
hook.
A suite equates to a single api-benchmark
test run. The suite
hook takes two parameters: a title (just for the output) and a callback function for configuring the suite. The callback will include all the other hooks which lets you configure this suite's services, routes, and options.
suite('My suite that tests the status route', () => {
// Configure the suite here
});
You can nest suites inside each other for cases where you have shared configurations.
suite('My suite that tests my-service', () => {
service('my-service', 'http://localhost:8080'); // All child suites use this same service.
suite('My suite that tests the status route', () => {
options(...);
route(...);
});
suite('My suite that tests the version route', () => {
options(...);
route(...);
});
});
Both the version and status suites since they don't define a service of their own will check there parent to find out what service to run the tests against. Nesting suites allows for sharable configurations.
The tests defined in the suite need to run against a running HTTP server, you can tell the suite where to find the server via the service
hook.
suite('My suite that tests my-service', () => {
service('my-service', 'http://localhost:8080');
options(...);
route(...);
});
The service
hook takes two parameters: a name (must be a valid ECMAScript object property name) and the URL to the service to run the tests against. The URL is to be the base URL which we can extend with the route paths.
If your hitting a development lap or production environment or a local environment you've manually started up then you can just put in the URL as a string as seen above; however, if the site is dynamic and needs to be brought up when the tests starts and thus might have a dynamic URL (random port?) you can pass in a callback function which will be called by the suite when it starts executing.
function findService() {...}
suite('My suite that tests my-service', () => {
service('my-service', findService);
options(...);
route(...);
});
The function you pass in must return a string consisting of the service URL.
Each suite can be configured with options defining how the test should be run. This includes how many samples, if the tests should be parallel or sequential, and the maximum number of concurrent requests if tests are to be done in parallel.
You can define these options via the options
hook. This hook is optional however as each option has a default value.
suite('Multiple requests in Sequence', () => {
options({
runMode: 'sequence',
minSamples: 200,
maxTime: 20,
});
route(...);
});
These options can also be nested in a parent suite if they are common.
suite('My suite that tests my-service', () => {
// All child suites use this same service and test options.
service('my-service', 'http://localhost:8080');
options({
runMode: 'sequence',
minSamples: 200,
maxTime: 20,
});
suite('My suite that tests the status route', () => {
route(...);
});
suite('My suite that tests the version route', () => {
route(...);
});
});
These options are the api-benchmark
options, for a list of valid options see the api-benchmark
documentation
To define a given test for the suite you need to define the route to test. This can be done via the route
hook.
The route
hook takes two parameters: A name (must be a valid ECMAScript object property name) and the relative route path.
suite('My suite that tests the status route', () => {
service('my-service', 'http://localhost:8080');
route('status', 'api/status');
});
This will tell api-benchmark
to run the tests per the suite options against the route api/status
attached to the service (i.e. http//localhost:8080/api/status
).
By default this will be a GET
and will always pass displaying the operation per second results; however, if you want to benchmark a different HTTP verb (i.e. POST
, PUT
, DELETE
) or want to define failure conditions (must be faster then Xms, must return with 200) you can pass in a route options object instead of the route string.
suite('My suite that tests the status route', () => {
route('status', {
method: 'get',
route: 'api/status',
expectedStatusCode: 200,
maxMean: 0.2, // 200ms
});
});
These options are the api-benchmark
route options, for a list of valid options see the api-benchmark
documentation
If the route you want to test contains a path variable, that is the route string is dynamic as it contains an ID which is generated by a before
hook you can define the route
property of the route options object as a function via the get
descriptor.
suite('My suite that tests the status route', () => {
let fileId;
before(() => (
createUser()
.then(uploadSmallFile)
.then((id) => (fileId = id))
));
route('status', {
method: 'get',
get route() {
return `api/file/${fileId}`;
},
expectedStatusCode: 200,
maxMean: 0.2, // 200ms
});
});
The route
property is expected to be a string and will not be invoked but rather read (i.e. doSomething(option.route)
) so you can not simply define it as a function; however, by marking a function with the get descriptor it becomes a getter and thus is implicitly invoked (i.e. option.route
vs. option.route()
) so when the library goes to access the property (i.e. doSomething(option.route)
) it will implicitly invoke the function and return the dynamically generated route string.
When running the tests you might need to setup the environment first. You might need to insert entries into the database if testing a DELETE
route or create users to log in as, you might even just simply need to bring up the server if testing locally. Like mocha
these setup/tear down steps can be handled via before
and after
hooks.
suite('Multiple requests in Sequence', () => {
before(() => {
// do Setup
});
after(() => {
// do tear down
});
service('my-service', 'http://localhost:8080');
options(...);
route(...);
});
A suite can also have multiple before
/after
hooks which will be executed in the order that they are defined in the suite.
suite('Multiple requests in Sequence', () => {
before(() => {...}); // runs first
before(() => {...}); // runs second
before(() => {...}); // runs last
after(() => {...}); // runs first
after(() => {...}); // runs last
service('my-service', 'http://localhost:8080');
options(...);
route(...);
});
These before
and after
hooks can be on each suite when suits are nested. These hooks will only be run once and will be run in descending order (parent suite hooks are run first followed by children).
suite('Status Route', () => {
service('my-service', 'http://localhost:8080');
before(() => {...});
after(() => {...});
suite('Multiple requests in Parallel', () => {
before(() => {...});
after(() => {...});
options(...);
route(...);
});
suite('Multiple requests in Sequence', () => {
before(() => {...});
after(() => {...});
options(...);
route(...);
});
});
In the above when the parallel suite begins first it will trigger its parents before
hooks then its own followed by the test then its after
hooks. Next the sequential test will start, as its parents before
hook have already run they will be skipped so it then runs its before
hooks followed by the test and ending with its after
hooks. Since there are no more sibling suites the parent suite's after
hooks will now be triggered. This is not unlike mocha
.
The before
and after
hooks take only one parameter and that is the callback function to execute when the hook is triggered. This callback function is run synchronously; however, if it returns a Promise
the execution will wait until the promise is resolved. Alternatively the passed in function can accept a done
callback so that it can tell the before
/after
hook when its finished.
suite('Multiple requests in Sequence', () => {
let server;
before((done) => {
server = require(`server`);
server.once('listening', () => {
done();
});
});
after((done) => {
server.close((err) => {
if (err) {
console.log('Error shutting down server');
} else {
console.log('Server shutdown');
}
done();
});
});
service('my-service', () => server.url);
options(...);
route(...);
});
If you have tests across a number of files having each file bring up and tear down your service is going to be a pain. If you need to bring up your service once at the beginning and only tear it down at the very end of the test run you can define a global setup script.
By convention api-bench-runner
will look for a special script called setup.js
which contains a single suite
with before
and after
hooks that it will use as the grandparent suite for all the files. That means that any before
hooks defined in this file will be triggered once at the start of the entire test run and any after
hooks will be triggered right before the tool finishes allowing for a one time global setup and tear down steps.
The setup.js
file is expected to live under the bench
folder of the working directory which the api-bench-runner
tool is running.
├── bench
│ ├── nested-test-example.js
│ ├── setup.js
│ └── simple-test-example.js
└── lib
└── server.js
If you run api-bench-runner
in the project root it will looks for bench/setup.js
and add it as the root suite for all the defined tests. If it is not found then no global setup/tear down hooks will be setup.
You can however tell api-bench-runner
where to local the global setup script via the --setup
flag if you choose to have a different folder structure.
├── test
│ ├── nested-test-example.bench.js
│ └── simple-test-example.bench.js
├── setup.js
└── lib
└── server.js
Given the above structure you can execute the following command to find all the right scripts:
> node_modules/.bin/api-bench-runner ./test/*.bench.js --setup ./setup.js
If your tests are not running correctly and you suspect there is something wrong with the hooks (did the service come up?) or otherwise just want to see more info on what is going on there are two options:
Each route
can be run in debug mode which simply displays more info to the terminal. If everything is working you just get pretty much the same output as what api-bench-runner
generates but on error you'll get more info.
suite('My suite that tests the status route', () => {
route('status', {
debug: true,
method: 'get',
route: 'api/status',
expectedStatusCode: 200,
maxMean: 0.2, // 200ms
});
});
The api-bench-runner
tool has logging although it does not log anything by default. The log messages are printed to the terminal so by default we log nothing to keep the terminal output clean. To see additional logging you can set the log level to debug
or tace
via the --level
flag.
> node_modules/.bin/api-bench-runner --level debug .
This will print out information about each stage of the tool; that is when it starts executing hooks or starts the test runs.
To run the tests for api-bench-runner
use the npm test
command.
Make sure to run npm install
first.
You can have the tests watch for changes and re-run automatically by using the command npm run watch
.
The Airbnb JavaScript Style Guide is used for this project so you must comply to that rule set. You can verify your changes are in compliance via the npm run lint
command.
This project does have a few alterations to the rules such as requiring the use of the use strict
pragma as we don't use babel
. The npm run lint
command enforces the alterations. To see all alterations look in the .eslintrc.json
files.
Bug reports and pull requests are welcome. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The tool is available as open source under the terms of the MIT License.