Skip to main content

Testing

Automated testing is considered an essential part of any serious software development effort. Automation makes it easy to repeat individual tests or test suites quickly and easily during development. This helps ensure that releases meet quality and performance goals. Automation helps increase coverage and provides a faster feedback loop to developers. Automation both increases the productivity of individual developers and ensures that tests are run at critical development lifecycle junctures, such as source code control check-in, feature integration, and version release.

Such tests often span a variety of types, including unit tests, end-to-end (e2e) tests, integration tests, and so on. While the benefits are unquestionable, it can be tedious to set them up. Octo strives to promote development best practices, including effective testing, so it includes features such as the following to help developers and teams build and automate tests. Octo:

  • Automatically scaffolds default unit tests for modules and e2e tests for applications.
  • Provides default tooling (such as a test runner that builds an isolated module/application loader).
  • Makes the Octo container system available in the testing environment for easily mocking factories.

You can use any testing framework that you like, as Octo doesn't force any specific tooling. Simply replace the elements needed (such as the test runner), and you will still enjoy the benefits of Octo's ready-made testing facilities.

info

The examples we provide here are written with the Jest framework.

What to expect

Your Octo application is written using Modules, which is what we aim to test. Within the module you manipulate models, import other modules, and write hooks.

Using Octo's testing framework, you will be able to test the exact doings of your Module - the models it manipulates, the resources it generates, and the order of transaction.

Setup

To get started, let's first install a testing framework. Here, we are using Jest.

npm install @types/jest jest ts-test --save-dev

Then, to setup jest, add its configuration in package.json, and add a test script.

package.json
{
"devDependencies": { ... },
"jest": {
"coverageDirectory": "./coverage",
"extensionsToTreatAsEsm": [
".ts"
],
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"moduleNameMapper": {
"(.+)\\.js": "$1"
},
"preset": "ts-jest/presets/default-esm",
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".*spec\\.ts$",
"transform": {
"^.+\\.ts$": [
"ts-jest",
{
"useESM": true
}
]
}
},
"name": "octo-starter-project",
"scripts": {
"build": "rimraf dist && tsc -p tsconfig.json",
"start": "node dist/main.js",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testRegex=\"\\.spec.ts$\""
},
"type": "module",
"version": "0.0.1"
}

Unit Testing

In the following example, we test the AppModule, which we introduced in the Hello World guide.

app.module.spec.ts
import { TestContainer, TestModuleContainer } from "@quadnix/octo";
import { OctoAwsCdkPackageMock } from "@quadnix/octo-aws-cdk";
import { AppModule } from "./app.module.js";

describe('AppModule UT', () => {
let testModuleContainer: TestModuleContainer;

beforeAll(async () => {
await TestContainer.create(
{
importFrom: [OctoAwsCdkPackageMock],
},
{ factoryTimeoutInMs: 500 },
);
});

beforeEach(async () => {
testModuleContainer = new TestModuleContainer();
await testModuleContainer.initialize();
});

afterEach(async () => {
await testModuleContainer.reset();
});

afterAll(async () => {
await TestContainer.reset();
});

it('should add a s3 website', async () => {
const appModule = new AppModule();
const app = await appModule.onInit();

expect((await testModuleContainer.commit(app)).resourceTransaction).toMatchInlineSnapshot();
});
});
info

Keep your test files located near the classes they test. Testing files should have a .spec suffix.

info

This example has just scratched the surface of what Octo provides for testing. A lot more options and methods are available at your disposal, and are better documented in their individual class API documentation.

TestContainer Initialization & Tear Down

beforeAll(async () => {
await TestContainer.create(
{
importFrom: [OctoAwsCdkPackageMock],
},
{ factoryTimeoutInMs: 500 },
);
});

The TestContainer class is initialized at the beginning of the test to create a new Container. It allows you to mock factories, and import other mocks.

The OctoAwsCdkPackageMock is one such pre-build set of mocks that mocks all AWS calls to ensure that your tests don't accidentally change resources in AWS.

In the TestContainer we also set the Container factory resolution timeout from 5s to 500ms in order to fail fast. Its just good practice, because if a factory is not resolved within a few milliseconds, that factory is possibly doing something wrong.

afterAll(async () => {
await TestContainer.reset();
});

Opposite of initialization, the TestContainer should be teared down before the next test file is run. This would just empty the Container, and reset all imported mocks.

TestModuleContainer Initialization & Tear Down

beforeEach(async () => {
testModuleContainer = new TestModuleContainer();
await testModuleContainer.initialize();
});

The TestModuleContainer class is initialized at the beginning of each test to create a new instance of Octo. It allows setting captures, inputs, mocks of other modules, and state providers.

It is essentially the replacement of main.ts we introduced in the Hello World guide. Use it to compose, begin, and commit transactions.

afterEach(async () => {
await testModuleContainer.reset();
});

Opposite of initialization, the TestModuleContainer should be teared down before the next test is run. This would reset all modules.

Test Blocks

it('should add a s3 website', async () => {
const appModule = new AppModule();
const app = await appModule.onInit();

expect((await testModuleContainer.commit(app)).resourceTransaction).toMatchInlineSnapshot();
});

A test block during unit testing should test a single module in isolation.

Here, we create a new instance of AppModule and run its onInit() method to change the state of app. Then we commit() the state of the app, and assert on the resources created in this test.

Summary

In this section we explored how you can write reliable, isolated, meaningful tests in Octo. We only test modules, the models it manipulates, and the resources it generates.