Skip to main content

Teraslice Test Harness

A helpful library for testing teraslice jobs, operations, and other components.

Note: Documentation is a WIP, currently on the basic usage and description is provided.

Installation

# Using yarn
yarn add --dev teraslice-test-harness
# Using npm
npm install --save-dev teraslice-test-harness

Available Test Harnesses

This package exports a few different test harnesses for running your operation.

Note: All TestHarnesses can take a path to the asset directory so it can the test can load and fully validate multiple different operations, if none are specified then it will assume it is running in a asset bundle.

SlicerTestHarness

A test harness for testing Operations that run on the Execution Controller, mainly Slicers.

This is useful for testing Slicers.

Usage:

const {
newTestJobConfig,
SlicerTestHarness,
} = require('teraslice-test-harness';

describe('Example Asset (Slicer)', () => {
const job = newTestJobConfig({
analytics: true,
operations: [
{
_op: 'simple-reader'
},
{
_op: 'noop'
}
]
});

let harness;

beforeEach(() => {
harness = new SlicerTestHarness(job, {
clients: [],
});

await harness.initialize();
});

afterEach(async () => {
await harness.shutdown();
});

it('should return a list of records', async () => {
const results = await harness.createSlices();
expect(Array.isArray(results)).toBe(true);
expect(results.length).toBe(10);

for (const result of results) {
expect(DataEntity.isDataEntity(result)).toBe(false);
expect(result.count).toEqual(10);
expect(result.super).toEqual('man');
}
});
});

WorkerTestHarness

A test harness for testing Operations that run on Workers, mainly Fetchers and Processors.

This is useful for testing Fetchers and Processors together or individually.

Usage:

const {
newTestJobConfig,
newTestSlice,
WorkerTestHarness,
} = require('teraslice-test-harness';

describe('Example Asset (Worker)', () => {
const clients = [
{
type: 'example',
create: () => ({
client: {
say() {
return 'hello';
},
},
}),
},
];

describe('when given a valid job config', () => {
const job = newTestJobConfig({
max_retries: 2,
analytics: true,
operations: [
{
_op: 'test-reader',
},
{
_op: 'noop',
},
],
});

const workerHarness = new WorkerTestHarness(job, {
clients,
});

beforeAll(() => workerHarness.initialize());
afterAll(() => workerHarness.shutdown());

it('should be able to call runSlice', async () => {
const result = await workerHarness.runSlice(newTestSlice());
expect(result).toBeArray();
expect(DataEntity.isDataEntityArray(result)).toBeTrue();
});

it('should be able to call runSlice with just a request object', async () => {
const result = await workerHarness.runSlice({ hello: true });
expect(result).toBeArray();
expect(DataEntity.isDataEntityArray(result)).toBeTrue();
});

it('should call slice retry', async () => {
const onSliceRetryEvent = jest.fn();
workerHarness.events.on('slice:retry', onSliceRetryEvent);
const err = new Error('oh no');

workerHarness.processors[0].handle
.mockClear()
.mockRejectedValueOnce(err)
.mockRejectedValueOnce(err);

const results = await workerHarness.runSlice({});
expect(results).toBeArray();

expect(onSliceRetryEvent).toHaveBeenCalledTimes(2);
expect(workerHarness.getOperation<NoopProcessor>('noop').handle).toHaveBeenCalledTimes(3);
});

it('should be able to call runSlice with fullResponse', async () => {
const result = await workerHarness.runSlice(newTestSlice(), {
fullResponse: true
});
expect(result.analytics).not.toBeNil();
expect(result.results).toBeArray();
});
});

describe('when using static method testProcessor', () => {
let harness;

beforeAll(async () => {
harness = WorkerTestHarness.testProcessor({ _op: 'noop' }, {});
await harness.initialize();
});

afterAll(async () => {
await harness.shutdown();
});

it('should return an instance of the test harness', () => {
expect(harness).toBeInstanceOf(WorkerTestHarness);
});

it('should be able run a slice', async () => {
const data = [{ some: 'data' }];
const results = await harness.runSlice(data);

expect(results).toEqual(data);
});
});

describe('when using static method testFetcher', () => {
let harness;

beforeAll(async () => {
harness = WorkerTestHarness.testFetcher({
_op: 'test-reader',
passthrough_slice: true
}, {});
await harness.initialize();
});

afterAll(async () => {
await harness.shutdown();
});

it('should return an instance of the test harness', () => {
expect(harness).toBeInstanceOf(WorkerTestHarness);
});

it('should be able to run a slice', async () => {
const data = [{ some: 'data' }];
const results = await harness.runSlice(data);

expect(results).toEqual(data);
});
});

describe('when testing flush', () => {
let harness;

beforeAll(async () => {
harness = WorkerTestHarness.testProcessor({ _op: 'simple-flush' }, {});
await harness.initialize();
});

afterAll(async () => {
await harness.shutdown();
});

it('should flush any remaining records', async () => {
const data = [{ some: 'data' }, { other: 'data' }];

const emptyResults = await harness.runSlice(data);
expect(emptyResults).toEqual([]);

const flushedResults = await harness.flush();
expect(flushedResults).toEqual(data);
});
});

describe('when testing flush with analytics', () => {
let harness;

beforeAll(async () => {
const job = newTestJobConfig({
max_retries: 0,
analytics: true,
operations: [
{
_op: 'test-reader',
passthrough_slice: true,
},
{ _op: 'simple-flush' },
],
});

harness = new WorkerTestHarness(job, {});

await harness.initialize();
});

afterAll(async () => {
await harness.shutdown();
});

it('should able return the result with analytics', async () => {
const data = [{ some: 'data' }, { other: 'data' }];

const emptyResults = await harness.runSlice(data);
expect(emptyResults).toEqual([]);

const flushedResults = await harness.flush({ fullResponse: true });

expect(flushedResults).toHaveProperty('results', data);
expect(flushedResults).toHaveProperty('status', 'flushed');
expect(flushedResults).toHaveProperty('analytics');
expect(flushedResults!.analytics).toContainKeys(['time', 'memory', 'size']);
});
});
});

JobTestHarness

A test harness for both the Slicer and Fetcher, utilizing both the Slicer and Worker test harnesses.

This is useful for testing Readers.

Usage:

const {
JobTestHarness,
newTestJobConfig,
newTestSlice,
} = require('teraslice-test-harness';

describe('Example Asset (Job)', () => {
const job = newTestJobConfig({
analytics: true,
operations:
{
_op: 'simple-reader'
},
{
_op: 'transformer',
action: 'inc',
key: 'scale',
incBy: 5,
},
{
_op: 'transformer',
action: 'inc',
key: 'scale',
incBy: 1,
}
]
});

let harness;

beforeEach(async () => {
harness = new JobTestHarness(job);

await harness.initialize();
});

afterEach(async () => {
await harness.shutdown();
});

it('should batches of results', async () => {
const batches = await harness.run();

expect(Array.isArray(batches)).toBe(true);
expect(batches.length).toBe(10);

for (const results of batches) {
expect(Array.isArray(results)).toBe(true);
expect(results.length).toBe(10);

for (const result of results) {
expect(DataEntity.isDataEntity(result)).toBe(true);
expect(result.scale).toBe(6);
}
}
});

it('should be finished for the second batch of slices', async () => {
const batches = await harness.run();

expect(Array.isArray(batches)).toBe(true);
expect(batches.length).toBe(10);

for (const results of batches) {
expect(Array.isArray(results)).toBe(true);
expect(results.length).toBe(10);

for (const result of results) {
expect(DataEntity.isDataEntity(result)).toBe(true);
expect(result.scale).toBe(6);
}
}
});
});

DownloadExternalAssets

Used to test processors in separate asset bundles. It downloads a zipped asset bundle to ./test/.cache/downloads and unzips the asset to ./test/.cache/assets. At this point assets must be in a github repository to be downloaded.

Since this is downloading additional files to the asset be sure to add ./test/.cache to the .gitignore file in the asset bundle.

Usage:

To use this functionality add DownloadExternalAssets to a global setup file, ./test/global.setup.js.

global.setup.js example:

const { DownloadExternalAsset } = require('teraslice-test-harness');

module.exports = async function getExternalAssets() {
const externalAssets = new DownloadExternalAsset();

await externalAssets.downloadExternalAsset('terascope/elasticsearch-assets@v2.2.0'); // external asset to test with
}

DownloadExternalAsset also accepts the asset without the version for example await externalAssets.downloadExternalAsset('terascope/elasticsearch-assets');. This will downloaded the latest release, including pre-releases. Use multiple lines to specify more than one asset.

await externalAssets.downloadExternalAsset('terascope/elasticsearch-assets@v2.2.0');
await externalAssets.downloadExternalAsset('terascope/kafka-assets@v2.8.2');

Make sure globalSetup: './test/global.setup.js', is included in the jest.config.js file.

Example of jest.config.js file with the globalSetup property:

'use strict';

module.exports = {
verbose: true,
testEnvironment: 'node',
...
globalSetup: './test/global.setup.js',
};

If set up properly the test will start by downloading the asset specified by the global setup file then proceed onto the tests.

Builtin Operations

Checkout these docs for a list of built-in operations.