Testing
Unchained Engine uses Node.js built-in test runner for both unit and integration tests. This guide covers how to test custom plugins, modules, and integrations.
Running Tests
All Tests
npm run test
Unit Tests Only
npm run test:run:unit
Integration Tests
npm run test:run:integration
Single Test File
# Unit test
node --test path/to/test.ts
# Integration test (from monorepo root)
node --no-warnings \
--env-file .env.tests \
--env-file-if-exists=.env \
--test-isolation=none \
--test-force-exit \
--test-global-setup=tests/helpers.js \
--test \
--test-concurrency=1 \
path/to/test.ts
Unit Testing
Unit tests validate the behavior callbacks passed to the registration factories. Keep domain logic in named functions, register those functions in boot code, and test them without initializing the global plugin registry.
Testing a Custom Plugin
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
const gateway = { charge: async (order) => ({ id: '' }) }; // your gateway SDK client
export const charge = async (configuration, context) => {
const result = await gateway.charge(context.order);
return { transactionId: result.id };
};
// Boot code:
// registerPaymentProvider({ adapterId: 'custom', type: 'GENERIC', charge });
describe('custom payment charge', () => {
it('returns the gateway transaction id', async () => {
gateway.charge = async () => ({ id: 'tx-1' });
const result = await charge([], { order: { _id: 'order-1' } });
assert.deepEqual(result, { transactionId: 'tx-1' });
});
});
Testing a Pricing Plugin
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
export const calculateMemberPrice = async (sheet, context) => {
if (context.product.tags?.includes('member-price')) {
sheet.addItem({ amount: -100, isTaxable: true, isNetPrice: true });
}
};
// Boot code:
// registerProductPricing({ adapterId: 'member-price', calculate: calculateMemberPrice });
describe('member pricing', () => {
it('adds the member discount row', async () => {
const rows = [];
const sheet = { addItem: (row) => rows.push(row) };
await calculateMemberPrice(sheet, { product: { tags: ['member-price'] } });
assert.equal(rows[0].amount, -100);
});
});
Integration Testing
Integration tests run against a live Unchained instance with MongoDB.
Test Setup
Integration tests run against a dedicated test harness, not an example app: the global setup in tests/helpers.js bootstraps a Fastify instance and startPlatform() (via tests/setup.js) with all plugins registered, mirroring the kitchensink configuration.
Environment
Integration tests load .env.tests with .env as an optional fallback (see the command above). The monorepo's .env.tests intentionally sets no MONGO_URL — the engine starts a mongodb-memory-server instance automatically when the variable is absent. A minimal .env.tests:
NODE_ENV=test
EMAIL_WEBSITE_NAME=Unchained
EMAIL_WEBSITE_URL=http://localhost:4010
EMAIL_FROM=noreply@unchained.local
UNCHAINED_TOKEN_SECRET=random-token-that-is-not-secret-at-all # must be at least 32 characters
# tests/setup.js assigns ROOT_URL from the dynamically allocated server port
# No MONGO_URL: mongodb-memory-server is started automatically
Set MONGO_URL only if you want to run tests against a real MongoDB instance.
Writing an Integration Test
import assert from 'node:assert/strict';
import { describe, it, before } from 'node:test';
import { setupDatabase, createLoggedInGraphqlFetch } from './helpers.js';
describe('Add a Product to the Cart', () => {
let graphqlFetch;
before(async () => {
await setupDatabase();
graphqlFetch = createLoggedInGraphqlFetch(); // admin client
});
it('creates a product and adds it to the cart', async () => {
// Create a product (title/slug live on the texts argument, not the product input)
const { data: { createProduct } } = await graphqlFetch({
query: `
mutation {
createProduct(
product: { type: SIMPLE_PRODUCT }
texts: [{ locale: "en", title: "Test Product" }]
) {
_id
}
}
`,
});
// Cart validation requires an active product
await graphqlFetch({
query: `
mutation PublishProduct($productId: ID!) {
publishProduct(productId: $productId) { _id }
}
`,
variables: { productId: createProduct._id },
});
// Add to cart
const { data: { addCartProduct } } = await graphqlFetch({
query: `
mutation AddToCart($productId: ID!) {
addCartProduct(productId: $productId, quantity: 1) {
_id
}
}
`,
variables: { productId: createProduct._id },
});
assert.ok(addCartProduct._id);
});
});
GraphQL Test Client
The test helpers provide authenticated GraphQL clients. createLoggedInGraphqlFetch(token) takes a bearer token string and defaults to the seeded admin token:
import {
setupDatabase,
createLoggedInGraphqlFetch,
createAnonymousGraphqlFetch,
} from './helpers.js';
await setupDatabase(); // wipes and reseeds all collections
const adminGraphqlFetch = createLoggedInGraphqlFetch(); // defaults to ADMIN_TOKEN
const userGraphqlFetch = createLoggedInGraphqlFetch('Bearer user-secret'); // seeded regular user
const anonymousGraphqlFetch = createAnonymousGraphqlFetch();
// Use for admin operations
const result = await adminGraphqlFetch({
query: '{ users { _id } }',
});
Testing Custom Modules
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { startPlatform } from '@unchainedshop/platform';
describe('Custom Module', () => {
let unchainedAPI;
it('should initialize', async () => {
const platform = await startPlatform({
modules: {
customModule: {
configure: async ({ db }) => ({
findItems: async () => [],
createItem: async (data) => ({ _id: 'new', ...data }),
}),
},
},
});
unchainedAPI = platform.unchainedAPI;
assert.ok(unchainedAPI.modules.customModule);
});
it('should create items', async () => {
const item = await unchainedAPI.modules.customModule.createItem({
name: 'Test',
});
assert.equal(item.name, 'Test');
});
});
Best Practices
- Use
.env.testsfor test-specific configuration to avoid affecting development data - Run with
--test-concurrency=1for integration tests to avoid race conditions - Clean up test data after each test suite to keep tests independent
- Test adapters in isolation before integration testing with the full platform
- Use
--test-isolation=nonefor integration tests that share platform state
Related
- Custom Modules - Build custom modules
- Director/Adapter Pattern - Plugin architecture
- Worker - Custom workers