Testing Foxx services

Foxx provides out of the box support for running tests against aninstalled service using an API similar tothe Mocha test runner.

Test files have full access to the service contextand all ArangoDB APIs but can not define Foxx routes.

Test files can be specified in the service manifestusing either explicit paths of each individual file or patterns that canmatch multiple files (even if multiple patterns match the same file,it will only be executed once):

  1. {
  2. "tests": [
  3. "some-specific-test-file.js",
  4. "test/**/*.js",
  5. "**/*.spec.js",
  6. "**/__tests__/**/*.js"
  7. ]
  8. }

To run a service’s tests you can usethe web interface,the Foxx CLI orthe Foxx HTTP API.Foxx will execute all test cases in the matching files andgenerate a report in the desired format.

Running tests in a production environment is not recommended andmay 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:

  1. "use strict";
  2. const { expect } = require("chai");
  3. // later
  4. expect("test".length).to.equal(4);

Alternatively ArangoDB also provides an implementation ofNode’s assert module:

  1. "use strict";
  2. const assert = require("assert");
  3. // later
  4. assert.equal("test".length, 4);

Test cases can be defined in any of the following ways using helper functionsinjected by Foxx when executing the test file:

Functional style

Test cases are defined using the it function and can be grouped intest suites using the describe function. Test suites can use thebefore and after functions to prepare and cleanup the suite andthe beforeEach and afterEach functions to prepare and cleanupeach 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 havethe aliases suiteSetup and suiteTeardown.

The beforeEach and afterEach functions also havethe aliases setup and teardown.

Note: These functions are automatically injected into the test file anddon’t have to be imported explicitly. The aliases can be used interchangeably.

  1. "use strict";
  2. const { expect } = require("chai");
  3. test("a single test case", () => {
  4. expect("test".length).to.equal(4);
  5. });
  6. describe("a test suite", () => {
  7. before(() => {
  8. // This runs before the suite's first test case
  9. });
  10. after(() => {
  11. // This runs after the suite's last test case
  12. });
  13. beforeEach(() => {
  14. // This runs before each test case of the suite
  15. });
  16. afterEach(() => {
  17. // This runs after each test case of the suite
  18. });
  19. it("is a test case in the suite", () => {
  20. expect(4).to.be.greaterThan(3);
  21. });
  22. it("is another test case in the suite", () => {
  23. expect(4).to.be.lessThan(5);
  24. });
  25. });
  26. suite("another test suite", () => {
  27. test("another test case", () => {
  28. expect(4).to.be.a("number");
  29. });
  30. });
  31. context("yet another suite", () => {
  32. specify("yet another case", () => {
  33. expect(4).to.not.equal(5);
  34. });
  35. });

Exports style

Test cases are defined as methods of plain objects assigned to test suiteproperties on the exports object:

  1. "use strict";
  2. const { expect } = require("chai");
  3. exports["this is a test suite"] = {
  4. "this is a test case": () => {
  5. expect("test".length).to.equal(4);
  6. }
  7. };

Methods named before, after, beforeEach and afterEach behave similarlyto the corresponding functions in the functional style described above:

  1. exports["a test suite"] = {
  2. before: () => {
  3. // This runs before the suite's first test case
  4. },
  5. after: () => {
  6. // This runs after the suite's last test case
  7. },
  8. beforeEach: () => {
  9. // This runs before each test case of the suite
  10. },
  11. afterEach: () => {
  12. // This runs after each test case of the suite
  13. },
  14. "a test case in the suite": () => {
  15. expect(4).to.be.greaterThan(3);
  16. },
  17. "another test case in the suite": () => {
  18. expect(4).to.be.lessThan(5);
  19. }
  20. };

Unit testing

The easiest way to make your Foxx service unit-testable is to extractcritical logic into side-effect-free functions and move these functions intomodules your tests (and router) can require:

  1. // in your router
  2. const lookupUser = require("../util/users/lookup");
  3. const verifyCredentials = require("../util/users/verify");
  4. const users = module.context.collection("users");
  5. router.post("/login", function (req, res) {
  6. const { username, password } = req.body;
  7. const user = lookupUser(username, users);
  8. verifyCredentials(user, password);
  9. req.session.uid = user._id;
  10. res.json({ success: true });
  11. });
  12. // in your tests
  13. const verifyCredentials = require("../util/users/verify");
  14. describe("verifyCredentials", () => {
  15. it("should throw when credentials are invalid", () => {
  16. expect(() => verifyCredentials(
  17. { authData: "whatever" },
  18. "invalid password"
  19. )).to.throw()
  20. });
  21. })

Integration testing

You should avoid running integration tests while a serviceis mounted in development mode as each requestwill cause the service to be reloaded.

You can use the @arangodb/request moduleto 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 contextwe can use this to make requests to the service itself:

  1. "use strict";
  2. const { expect } = require("chai");
  3. const request = require("@arangodb/request");
  4. const { baseUrl } = module.context;
  5. describe("this service", () => {
  6. it("should say 'Hello World!' at the index route", () => {
  7. const response = request.get(baseUrl);
  8. expect(response.status).to.equal(200);
  9. expect(response.body).to.equal("Hello World!");
  10. });
  11. it("should greet us with name", () => {
  12. const response = request.get(`${baseUrl}/Steve`);
  13. expect(response.status).to.equal(200);
  14. expect(response.body).to.equal("Hello Steve!");
  15. });
  16. });

An implementation passing the above tests could look like this:

  1. "use strict";
  2. const createRouter = require("@arangodb/foxx/router");
  3. const router = createRouter();
  4. module.context.use(router);
  5. router.get((req, res) => {
  6. res.write("Hello World!");
  7. })
  8. .response(["text/plain"]);
  9. router.get("/:name", (req, res) => {
  10. res.write(`Hello ${req.pathParams.name}!`);
  11. })
  12. .response(["text/plain"]);