Interfaces

Testing Interfaces

Learn how to test interfaces in AntelopeJS, ensuring your modules work seamlessly with the defined contracts

Overview

Testing interfaces in AntelopeJS is crucial to ensure that your modules work as expected, so it doesn't limit refactoring and evolution of your codebase. This guide covers how to create, update, and test interfaces effectively.

Defining tests

Environment setup

To tell AntelopeJS how to run your tests, you need to add a test attribute in the AntelopeJS configuration, usually located in package.json:

{
  "name": "my-module",
  "version": "0.0.1",
  "scripts": {
    "test": "npx tsc && ajs module test"
  },
  "antelopeJs": {
    "test": {
      "project": "src/loader.test.js",
      "folder": "dist/test"
    },
    [...other antelopeJS configs...]
  }
  [...other package.json properties...]
}

In the test configuration, there are two attributes:

  • project: This is the entry point for your tests. It should point to a JavaScript file that sets up the testing environment. For this documentation, we will use src/loader.test.js as an example.
  • folder: This is the directory where your compiled test files will be generated. For this documentation, we will use dist/test as an example.
The test configuration allows you to specify exactly how your tests should be executed and where the compiled test files should be placed. This separation ensures that your test environment is properly isolated from your main application code.

Test loader

The test loader is responsible for setting up the testing environment and cleaning up the artifacts that might be left after the tests are run.

It consists of two main parts:

module.exports.setup = async function() { }
module.exports.cleanup = async function() { }

Setup function

Testing requires a list of modules used by the interfaces being tested. This is done by injecting the configuration in the module setup.

module.exports.setup = async function() {
  return {
    cacheFolder: '.antelope/cache',
    modules: {
      local: {                // Module being tested
        source: {
          type: 'local',
          path: '.',
        },
      },
      mongodb: {              // Dependency module
        source: {
          type: 'git',
          remote: '[email protected]:AntelopeJS/mongodb.git',
          branch: 'main',
          installCommand: ['pnpm i', 'npx tsc'],
        },
        config: {
          url: mongod.getUri(),
        },
      },
      //[...other dependencies...]
    },
  }
};
The setup function should include all modules that your interface depends on. This ensures that your tests run in an environment that closely matches your production setup.

If needed, you can also add additional setup logic, such as starting a MongoDB instance or other services required for your tests.

const { MongoMemoryServer } = require('mongodb-memory-server-core');

let mongod;
module.exports.setup = async function() {
  mongod = await MongoMemoryServer.create();

  //[...dependencies setup ...]
}
When using external services like databases in your tests, always use in-memory or test-specific instances to avoid affecting your development or production data, or your tests might fail or succeed due to external factors.

Cleanup function

The cleanup function is responsible for cleaning up any resources used during the tests, such as stopping the MongoDB instance.

module.exports.cleanup = async function() {
  await mongod.stop();
};
Always implement proper cleanup to prevent resource leaks and ensure that subsequent test runs start with a clean state.

Writing tests

Place your tests in the src/test directory (or the directory specified in the folder attribute of the AntelopeJS configuration).

Every test file from this directory or subdirectories will be executed by the AntelopeJS test runner.

Tests are written using Mocha to create the test suite, and Chai for assertions.

Mocha provides a flexible testing framework with support for both synchronous and asynchronous tests, while Chai offers a comprehensive assertion library with multiple assertion styles (expect, should, assert).

Here is an example of a simple test file, operations.test.ts:

import { expect } from 'chai';

describe('Math - Basic operations', () => {
  it('add 2 to 10', async () => addTwoToTen());
  it('divide 0 by 0', async () => divideZeroByZero());
});

async function addTwoToTen() {
  const result = 10 + 2;
  expect(result).to.equal(12);
}

async function divideZeroByZero() {
    const result = 0 / 0;
    expect(result).to.be.NaN;
}
For interface testing, focus on testing the contract that your interface defines. This includes testing both successful operations and error conditions to ensure robust behavior.

Running tests

To run your tests, use the following command:

ajs module test

This command will compile your TypeScript files and execute the tests defined in the src/test directory.

Test output

After running the tests, you will see the output in the console, indicating which tests passed and which failed. The output will look something like this:

  Math - Basic operations
    ✓ add 2 to 10
    ✓ divide 0 by 0

  2 passing (10ms)