Testing Foxx services
Foxx provides out of the box support for running tests against an installed service using an API similar to the Mocha test runner.
Test files have full access to the service context and all ArangoDB APIs but cannot define Foxx routes.
Test files can be specified in the service manifest using either explicit paths of each individual file or patterns that can match multiple files (even if multiple patterns match the same file, it will only be executed once):
{
"tests": [
"some-specific-test-file.js",
"test/**/*.js",
"**/*.spec.js",
"**/__tests__/**/*.js"
]
}
To run a service’s tests you can use the web interface, the Foxx CLI or the Foxx HTTP API. Foxx will execute all test cases in the matching files and generate a report in the desired format.
Running tests in a production environment is not recommended and may result in data loss if the tests involve database access.
Writing tests
ArangoDB bundles the chai
library,
which can be used to define test assertions:
"use strict";
const { expect } = require("chai");
// later
expect("test".length).to.equal(4);
Alternatively ArangoDB also provides an implementation of
Node’s assert
module:
"use strict";
const assert = require("assert");
// later
assert.equal("test".length, 4);
Test cases can be defined in any of the following ways using helper functions injected by Foxx when executing the test file:
Functional style
Test cases are defined using the it
function and can be grouped in
test suites using the describe
function. Test suites can use the
before
and after
functions to prepare and cleanup the suite and
the beforeEach
and afterEach
functions to prepare and cleanup
each test case individually.
The it
function also has the aliases test
and specify
.
The describe
function also has the aliases suite
and context
.
The before
and after
functions also have
the aliases suiteSetup
and suiteTeardown
.
The beforeEach
and afterEach
functions also have
the aliases setup
and teardown
.
Note: These functions are automatically injected into the test file and don’t have to be imported explicitly. The aliases can be used interchangeably.
"use strict";
const { expect } = require("chai");
test("a single test case", () => {
expect("test".length).to.equal(4);
});
describe("a test suite", () => {
before(() => {
// This runs before the suite's first test case
});
after(() => {
// This runs after the suite's last test case
});
beforeEach(() => {
// This runs before each test case of the suite
});
afterEach(() => {
// This runs after each test case of the suite
});
it("is a test case in the suite", () => {
expect(4).to.be.greaterThan(3);
});
it("is another test case in the suite", () => {
expect(4).to.be.lessThan(5);
});
});
suite("another test suite", () => {
test("another test case", () => {
expect(4).to.be.a("number");
});
});
context("yet another suite", () => {
specify("yet another case", () => {
expect(4).to.not.equal(5);
});
});
Exports style
Test cases are defined as methods of plain objects assigned to test suite
properties on the exports
object:
"use strict";
const { expect } = require("chai");
exports["this is a test suite"] = {
"this is a test case": () => {
expect("test".length).to.equal(4);
}
};
Methods named before
, after
, beforeEach
and afterEach
behave similarly
to the corresponding functions in the functional style described above:
exports["a test suite"] = {
before: () => {
// This runs before the suite's first test case
},
after: () => {
// This runs after the suite's last test case
},
beforeEach: () => {
// This runs before each test case of the suite
},
afterEach: () => {
// This runs after each test case of the suite
},
"a test case in the suite": () => {
expect(4).to.be.greaterThan(3);
},
"another test case in the suite": () => {
expect(4).to.be.lessThan(5);
}
};
Unit testing
The easiest way to make your Foxx service unit-testable is to extract critical logic into side-effect-free functions and move these functions into modules your tests (and router) can require:
// in your router
const lookupUser = require("../util/users/lookup");
const verifyCredentials = require("../util/users/verify");
const users = module.context.collection("users");
router.post("/login", function (req, res) {
const { username, password } = req.body;
const user = lookupUser(username, users);
verifyCredentials(user, password);
req.session.uid = user._id;
res.json({ success: true });
});
// in your tests
const verifyCredentials = require("../util/users/verify");
describe("verifyCredentials", () => {
it("should throw when credentials are invalid", () => {
expect(() => verifyCredentials(
{ authData: "whatever" },
"invalid password"
)).to.throw()
});
})
Integration testing
You should avoid running integration tests while a service is mounted in development mode as each request will cause the service to be reloaded.
You can use the @arangodb/request
module
to let tests talk to routes of the same service.
When the request module is used with a path instead of a full URL,
the path is resolved as relative to the ArangoDB instance.
Using the baseUrl
property of the service context
we can use this to make requests to the service itself:
"use strict";
const { expect } = require("chai");
const request = require("@arangodb/request");
const { baseUrl } = module.context;
describe("this service", () => {
it("should say 'Hello World!' at the index route", () => {
const response = request.get(baseUrl);
expect(response.status).to.equal(200);
expect(response.body).to.equal("Hello World!");
});
it("should greet us with name", () => {
const response = request.get(`${baseUrl}/Steve`);
expect(response.status).to.equal(200);
expect(response.body).to.equal("Hello Steve!");
});
});
An implementation passing the above tests could look like this:
"use strict";
const createRouter = require("@arangodb/foxx/router");
const router = createRouter();
module.context.use(router);
router.get((req, res) => {
res.write("Hello World!");
})
.response(["text/plain"]);
router.get("/:name", (req, res) => {
res.write(`Hello ${req.pathParams.name}!`);
})
.response(["text/plain"]);