Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

[WIP] Integration Test Framework #168

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"clean": "shx rm -rf lib 0x_mesh/db",
"build": "tsc -p tsconfig.json",
"test": "ETHEREUM_RPC_URL=http://localhost:8545 mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 100000 --exit",
"test": "ETHEREUM_RPC_URL=http://localhost:8545 mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 1000000 --exit",
"dev": "nodemon -r dotenv/config src/index.ts | pino-pretty",
"dev:service:http": "nodemon -r dotenv/config src/runners/http_service_runner.ts | pino-pretty",
"dev:service:sra_http": "nodemon -r dotenv/config src/runners/http_sra_service_runner.ts | pino-pretty",
Expand Down Expand Up @@ -42,6 +42,7 @@
"@types/lodash": "^4.14.137",
"@types/mocha": "^5.2.7",
"@types/pino": "^5.8.13",
"@types/rimraf": "^3.0.0",
"@types/supertest": "^2.0.8",
"@types/web3": "^1.0.19",
"@types/ws": "^6.0.2",
Expand All @@ -52,6 +53,7 @@
"nodemon": "^1.19.4",
"pino-pretty": "^3.2.2",
"prettier": "^1.18.2",
"rimraf": "^3.0.2",
"shx": "^0.3.2",
"supertest": "^4.0.2",
"ts-node": "^8.4.1",
Expand Down
6 changes: 5 additions & 1 deletion src/db_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { Connection, createConnection } from 'typeorm';

import { config } from './ormconfig';

const connection = createConnection(config);
let connection: Connection;

/**
* Creates the DB connnection to use in an app
*/
export async function getDBConnectionAsync(): Promise<Connection> {
if (!connection) {
connection = await createConnection(config);
}
return connection;
}
94 changes: 42 additions & 52 deletions test/app_test.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,47 @@
import { BlockchainLifecycle, web3Factory } from '@0x/dev-utils';
import { runMigrationsOnceAsync } from '@0x/migrations';
import { Web3ProviderEngine } from '@0x/subproviders';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as HttpStatus from 'http-status-codes';
import 'mocha';
import * as request from 'supertest';

import { getAppAsync, getDefaultAppDependenciesAsync } from '../src/app';
import * as config from '../src/config';
import { DEFAULT_PAGE, DEFAULT_PER_PAGE, SRA_PATH } from '../src/constants';

import { expect } from './utils/expect';
import { ACTIONS, ASSERTIONS, createTestManager, TestCaseType } from './framework';

let app: Express.Application;

let web3Wrapper: Web3Wrapper;
let provider: Web3ProviderEngine;
let accounts: string[];
let blockchainLifecycle: BlockchainLifecycle;

describe('app test', () => {
before(async () => {
// start ganache and run contract migrations
const ganacheConfigs = {
shouldUseInProcessGanache: false,
shouldAllowUnlimitedContractSize: true,
rpcUrl: config.ETHEREUM_RPC_URL,
};
provider = web3Factory.getRpcProvider(ganacheConfigs);
web3Wrapper = new Web3Wrapper(provider);
blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
await blockchainLifecycle.startAsync();
accounts = await web3Wrapper.getAvailableAddressesAsync();
const owner = accounts[0];
await runMigrationsOnceAsync(provider, { from: owner });

const dependencies = await getDefaultAppDependenciesAsync(provider, config);
// start the 0x-api app
app = await getAppAsync({ ...dependencies }, config);
});
it('should not be undefined', () => {
expect(app).to.not.be.undefined();
});
it('should respond to GET /sra/orders', async () => {
await request(app)
.get(`${SRA_PATH}/orders`)
.expect('Content-Type', /json/)
.expect(HttpStatus.OK)
.then(response => {
expect(response.body.perPage).to.equal(DEFAULT_PER_PAGE);
expect(response.body.page).to.equal(DEFAULT_PAGE);
expect(response.body.total).to.equal(0);
expect(response.body.records).to.deep.equal([]);
});
});
});
const manager = createTestManager();
const suite: TestCaseType[] = [
{
description: 'should respond to GET /sra/orders',
action: {
actionType: ACTIONS.HTTP_GET,
input: {
route: `${SRA_PATH}/orders`,
},
},
assertions: [
{
assertionType: ASSERTIONS.EQUALS,
input: {
path: 'status',
value: HttpStatus.OK,
},
},
{
assertionType: ASSERTIONS.MATCHES,
input: {
path: 'type',
value: /json/,
},
},
{
assertionType: ASSERTIONS.EQUALS,
input: {
path: 'body',
value: {
perPage: DEFAULT_PER_PAGE,
page: DEFAULT_PAGE,
total: 0,
records: [],
},
},
},
],
},
];
manager.executeTestSuite('app test', suite);
12 changes: 12 additions & 0 deletions test/framework/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as request from 'supertest';

const API_HTTP_ADDRESS = 'http://localhost:3000';

/**
* Makes a HTTP GET request.
* @param input Specifies the route and the base URL that should be used to make
* the HTTP GET request.
*/
export async function httpGetAsync(input: { route: string; baseURL?: string }): Promise<request.Response> {
return request(input.baseURL || API_HTTP_ADDRESS).get(input.route);
}
37 changes: 37 additions & 0 deletions test/framework/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect } from '../utils/expect';

/**
* Asserts that a specified path of the actual result is equal to an expected value.
* @param actual The result of the previous action.
* @param input Specifies both the path and the expected value of the path that
* should be compared with the actual result. If the path is empty, this
* function will compare the full `actual` object to `input.value`.
*/
export async function assertEqualsAsync(
actual: any,
input: {
path?: string;
value: any;
},
): Promise<void> {
const actual_ = input.path ? actual[input.path] : actual;
expect(actual_).to.be.deep.eq(input.value);
}

/**
* Asserts that a specified field of the actual result is equal to an expected value.
* @param actual The result of the previous action.
* @param input Specifies both the path and the expected value of the path that
* should be compared with the actual result. If the path is empty, this
* function will compare the full `actual` object to `input.value`.
*/
export async function assertMatchesAsync(
actual: any,
input: {
path?: string;
value: RegExp;
},
): Promise<void> {
const actual_ = input.path ? actual[input.path] : actual;
expect(actual_).to.match(input.value);
}
34 changes: 34 additions & 0 deletions test/framework/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as actions from './actions';
import * as assertions from './assertions';
import { TestCase, TestManager } from './test_manager';

export const ACTIONS = {
HTTP_GET: 'httpGetAsync' as ActionType,
};

export const ASSERTIONS = {
EQUALS: 'assertEqualsAsync' as AssertionType,
MATCHES: 'assertMatchesAsync' as AssertionType,
};

export type ActionType = 'httpGetAsync';
export type AssertionType = 'assertEqualsAsync' | 'assertMatchesAsync';
export type TestCaseType = TestCase<ActionType, AssertionType>;

/**
* Constructs the default `TestManager` class, which uses strict type declarations
* to improve DevEx.
* @returns TestManager Returns a `TestManager` that has been given default actions
* and assertions.
*/
export function createTestManager(): TestManager<ActionType, AssertionType> {
return new TestManager(
{
httpGetAsync: actions.httpGetAsync,
},
{
assertEqualsAsync: assertions.assertEqualsAsync,
assertMatchesAsync: assertions.assertMatchesAsync,
},
);
}
99 changes: 99 additions & 0 deletions test/framework/test_manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import 'mocha';

import { LoggingConfig, setupApiAsync, teardownApiAsync } from '../utils/deployment';

export interface ActionInfo<TType extends string> {
actionType: TType;
input?: any;
inputPath?: string;
outputPath?: string;
}

export interface AssertionInfo<TType extends string> {
assertionType: TType;
input: any;
}

export interface TestCase<TActionType extends string, TAssertionType extends string> {
description: string;
action: ActionInfo<TActionType>;
assertions: Array<AssertionInfo<TAssertionType>>;
}

type Action = (input: any) => Promise<any>;
type Assertion = (actualResult: any, input: any) => Promise<void>;

export class TestManager<TActionType extends string, TAssertionType extends string> {
protected _data: any = {};

constructor(
protected _actionsAsync: Record<TActionType, Action>,
protected _assertionsAsync: Record<TAssertionType, Assertion>,
protected readonly _loggingConfig?: LoggingConfig,
) {}

/**
* NOTE(jalextowle): This function cannot be called from an `async` function.
* This will lead to undefined behavior and is a limitation
* of Mocha. Source: https://github.com/mochajs/mocha/issues/3347
* Executes a test suite defined by the appropriate data structure.
* @param description A meaningful description of the test suite's purpose.
* @param testSuite The set of test cases that should be executed (in order)
* in this suite.
*/
public executeTestSuite(description: string, testSuite: Array<TestCase<TActionType, TAssertionType>>): void {
if (!testSuite.length) {
throw new Error(`[test-manager] suite '${description}' is empty`);
}

// Execute each of the test cases.
describe(description, () => {
beforeEach(async () => {
// Setup the 0x-api instance.
await setupApiAsync(this._loggingConfig);
});

afterEach(async () => {
// Teardown the 0x-api instance.
await teardownApiAsync(this._loggingConfig);
});

for (const testCase of testSuite) {
it(testCase.description, async () => {
await this._executeTestCaseAsync(testCase);
});
}
});
}

protected async _executeTestCaseAsync(testCase: TestCase<TActionType, TAssertionType>): Promise<void> {
const actionResult = await this._executeActionAsync(testCase.action);
for (const assertion of testCase.assertions) {
await this._executeAssertionAsync(assertion, actionResult);
}
}

protected async _executeActionAsync(action: ActionInfo<TActionType>): Promise<any> {
const actionAsync = this._actionsAsync[action.actionType];
if (!actionAsync) {
throw new Error(`[test-manager] action ${action.actionType} is not registered`);
}
const input = action.inputPath ? this._data[action.inputPath] : {};
const result = await actionAsync({
...input,
...action.input,
});
if (action.outputPath) {
this._data[action.outputPath] = result;
}
return result;
}

protected async _executeAssertionAsync(assertion: AssertionInfo<TAssertionType>, actualResult: any): Promise<void> {
const assertionAsync = this._assertionsAsync[assertion.assertionType];
if (!assertionAsync) {
throw new Error(`[test-manager] assertion ${assertion.assertionType} is not registered`);
}
return assertionAsync(actualResult, assertion.input);
}
}
Loading