From 140e7dd713301b63a458b5914cd51c36f32fd59a Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 8 Apr 2020 16:57:29 -0500 Subject: [PATCH 1/7] Added the setup and teardown functions for the 0x-api --- package.json | 2 +- src/db_connection.ts | 6 +- test/app_test.ts | 20 ++----- test/utils/deployment.ts | 121 +++++++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 5 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 test/utils/deployment.ts diff --git a/package.json b/package.json index a747b1875..856d2e2d6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/db_connection.ts b/src/db_connection.ts index 0a670839d..9430b021b 100644 --- a/src/db_connection.ts +++ b/src/db_connection.ts @@ -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 { + if (!connection) { + connection = await createConnection(config); + } return connection; } diff --git a/test/app_test.ts b/test/app_test.ts index 91ae0974d..e1594d385 100644 --- a/test/app_test.ts +++ b/test/app_test.ts @@ -1,7 +1,5 @@ -import { BlockchainLifecycle, web3Factory } from '@0x/dev-utils'; -import { runMigrationsOnceAsync } from '@0x/migrations'; +import { web3Factory } from '@0x/dev-utils'; 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'; @@ -10,35 +8,29 @@ import { getAppAsync, getDefaultAppDependenciesAsync } from '../src/app'; import * as config from '../src/config'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE, SRA_PATH } from '../src/constants'; +import { setupDependenciesAsync, teardownDependenciesAsync } from './utils/deployment'; import { expect } from './utils/expect'; 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 }); - + await setupDependenciesAsync(); const dependencies = await getDefaultAppDependenciesAsync(provider, config); // start the 0x-api app app = await getAppAsync({ ...dependencies }, config); }); + after(async () => { + await teardownDependenciesAsync(); + }); it('should not be undefined', () => { expect(app).to.not.be.undefined(); }); diff --git a/test/utils/deployment.ts b/test/utils/deployment.ts new file mode 100644 index 000000000..b8f2b4c03 --- /dev/null +++ b/test/utils/deployment.ts @@ -0,0 +1,121 @@ +import { logUtils as log } from '@0x/utils'; +import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; +import { resolve as resolvePath } from 'path'; + +const apiRootDir = resolvePath(`${__dirname}/../../../`); + +let yarnStartProcess: ChildProcessWithoutNullStreams; + +/** + * The configuration object that provides information on how verbose the logs + * should be. + * @param shouldPrintApiLogs Whether or not the 0x-api logs should be surfaced. + * @param shouldPrintDependencyLogs Whether or not the 0x-api's dependencies + * should surface their logs. + * TODO(jalextowle): It would be a good improvement to be able to specify log + * files where the logs should actually be written. + */ +export interface LoggingConfig { + shouldPrintApiLogs?: boolean; + shouldPrintDependencyLogs?: boolean; +} + +/** + * Sets up a 0x-api instance. + * @param logConfig Whether or not the logs from the setup functions should + * be printed. + */ +export async function setupApiAsync(logConfig: LoggingConfig = {}): Promise { + if (yarnStartProcess) { + throw new Error('Old 0x-api instance has not been torn down'); + } + await setupDependenciesAsync(logConfig.shouldPrintDependencyLogs || false); + yarnStartProcess = spawn('yarn', ['start'], { + cwd: apiRootDir, + }); + if (logConfig.shouldPrintApiLogs) { + yarnStartProcess.stdout.on('data', chunk => { + neatlyPrintChunk('[0x-api]', chunk); + }); + yarnStartProcess.stderr.on('data', chunk => { + neatlyPrintChunk('[0x-api | error]', chunk); + }); + } + // Wait for the API to boot up + // HACK(jalextowle): This should really be replaced by log-scraping, but it + // does appear to work for now. + await sleepAsync(2500); // tslint:disable-line:custom-no-magic-numbers +} + +/** + * Tears down the old 0x-api instance. + * @param logConfig Whether or not the logs from the teardown functions should + * be printed. + */ +export async function teardownApiAsync(logConfig: LoggingConfig = {}): Promise { + if (!yarnStartProcess) { + throw new Error('There is no 0x-api instance to tear down'); + } + yarnStartProcess.kill(); + await teardownDependenciesAsync(logConfig.shouldPrintDependencyLogs || false); +} + +/** + * Sets up 0x-api's dependencies. + * @param shouldPrintLogs Whether or not the logs from `docker-compose up` + * should be printed. + */ +export async function setupDependenciesAsync(shouldPrintLogs: boolean = false): Promise { + const up = spawn('docker-compose', ['up'], { + cwd: apiRootDir, + env: { + ...process.env, + ETHEREUM_RPC_URL: 'http://ganache:8545', + ETHEREUM_CHAIN_ID: '1337', + }, + }); + if (shouldPrintLogs) { + up.stdout.on('data', chunk => { + neatlyPrintChunk('[docker-compose up]', chunk); + }); + up.stderr.on('data', chunk => { + neatlyPrintChunk('[docker-compose up | error]', chunk); + }); + } + // Wait for the dependencies to boot up. + // HACK(jalextowle): This should really be replaced by log-scraping, but it + // does appear to work for now. + await sleepAsync(10000); // tslint:disable-line:custom-no-magic-numbers +} + +/** + * Tears down 0x-api's dependencies. + * @param shouldPrintLogs Whether or not the logs from `docker-compose down` + * should be printed. + */ +export async function teardownDependenciesAsync(shouldPrintLogs: boolean = false): Promise { + const down = spawn('docker-compose', ['down'], { + cwd: apiRootDir, + }); + if (shouldPrintLogs) { + down.stdout.on('data', chunk => { + neatlyPrintChunk('[docker-compose down]', chunk); + }); + down.stderr.on('data', chunk => { + neatlyPrintChunk('[docker-compose down | error]', chunk); + }); + } +} + +function neatlyPrintChunk(prefix: string, chunk: Buffer): void { + const data = chunk.toString().split('\n'); + data.filter((datum: string) => datum !== '').map((datum: string) => { + log.log(prefix, datum.trim()); + }); +} + +async function sleepAsync(duration: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, duration); + }); +} diff --git a/tsconfig.json b/tsconfig.json index 96970adb3..1c5526e0c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "noUnusedLocals": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "sourceMap": false, + "sourceMap": true, "typeRoots": ["./node_modules/@0x/typescript-typings/types", "./node_modules/@types"], "noUnusedParameters": true, "resolveJsonModule": true From 54e87783f54d0dbcf775137b2020cd42803a7e69 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 14 Apr 2020 15:53:14 -0500 Subject: [PATCH 2/7] Created the `TestManager` and an example test --- test/app_test.ts | 2 + test/new_app_test.ts | 29 ++++++++++++ test/utils/test_manager.ts | 96 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 test/new_app_test.ts create mode 100644 test/utils/test_manager.ts diff --git a/test/app_test.ts b/test/app_test.ts index e1594d385..b9729c562 100644 --- a/test/app_test.ts +++ b/test/app_test.ts @@ -30,6 +30,8 @@ describe('app test', () => { }); after(async () => { await teardownDependenciesAsync(); + // NOTE(jalextowle): The app should be torn down. I'm not going to worry + // about it because this test will be refactored so this will be a non-issue. }); it('should not be undefined', () => { expect(app).to.not.be.undefined(); diff --git a/test/new_app_test.ts b/test/new_app_test.ts new file mode 100644 index 000000000..1600aeef0 --- /dev/null +++ b/test/new_app_test.ts @@ -0,0 +1,29 @@ +import { TestCase, TestManager } from './utils/test_manager'; + +const sleepActionAsync = async (time: number) => { + return new Promise(resolve => { + setTimeout(resolve, time); + }); +}; +const manager = new TestManager( + { + sleep: sleepActionAsync, + }, + { + nothing: () => true, + }, +); +const suite: TestCase[] = [ + { + description: 'sleep for 5s', + action: { + actionType: 'sleep', + input: 5000, + }, + assertion: { + assertionType: 'nothing', + expectedResult: {}, + }, + }, +]; +manager.executeTestSuite('sleep assertion', suite); diff --git a/test/utils/test_manager.ts b/test/utils/test_manager.ts new file mode 100644 index 000000000..90e107a45 --- /dev/null +++ b/test/utils/test_manager.ts @@ -0,0 +1,96 @@ +import 'mocha'; + +import { setupApiAsync, teardownApiAsync } from './deployment'; +import { expect } from './expect'; + +// FIXME(jalextowle): Lock this down to only the actions that have been created. +export type ActionType = string; + +// FIXME(jalextowle): Lock this down to only the assetions that have been created. +export type AssertionType = string; + +// tslint:disable-next-line:interface-over-type-literal +export type ActionResult = {} | undefined; + +export interface ActionInfo { + actionType: ActionType; + // FIXME(jalextowle): Lock this down to only the actions that have been created. + input: any; +} + +export interface AssertionInfo { + assertionType: AssertionType; + expectedResult: ActionResult; +} + +export interface TestCase { + description: string; + action: ActionInfo; + assertion: AssertionInfo; +} + +type Action = (input: any) => Promise; +type Assertion = (expectedResult: ActionResult, actualResult: ActionResult) => boolean; + +export class TestManager { + constructor( + protected _actionsAsync: Record, + protected _assertionsAsync: Record, + ) {} + + /** + * 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: TestCase[]): 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(); + }); + + afterEach(async () => { + // Teardown the 0x-api instance. + await teardownApiAsync(); + }); + + for (const testCase of testSuite) { + it(testCase.description, async () => { + await this._executeTestCaseAsync(testCase); + }); + } + }); + } + + protected async _executeTestCaseAsync(testCase: TestCase): Promise { + const actionResult = await this._executeActionAsync(testCase.action); + const didAssertionPass = await this._executeAssertionAsync(testCase.assertion, actionResult); + expect(didAssertionPass).to.be.true(); + } + + protected async _executeActionAsync(action: ActionInfo): Promise { + const actionAsync = this._actionsAsync[action.actionType]; + if (!actionAsync) { + throw new Error('[test-manager] action is not registered'); + } + return actionAsync(action.input); + } + + protected async _executeAssertionAsync(assertion: AssertionInfo, actualResult: ActionResult): Promise { + const assertionAsync = this._assertionsAsync[assertion.assertionType]; + if (!assertionAsync) { + throw new Error('[test-manager] assertion is not registered'); + } + return assertionAsync(assertion.expectedResult, actualResult); + } +} From 2314488b00afb13557f86605b97b56c254fc1089 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 14 Apr 2020 18:39:32 -0500 Subject: [PATCH 3/7] Refactored existing test to use framework --- test/app_test.ts | 87 ++++++++++++++++++++------------------ test/new_app_test.ts | 29 ------------- test/utils/deployment.ts | 16 +++---- test/utils/test_manager.ts | 17 +++++--- 4 files changed, 63 insertions(+), 86 deletions(-) delete mode 100644 test/new_app_test.ts diff --git a/test/app_test.ts b/test/app_test.ts index b9729c562..b879862a1 100644 --- a/test/app_test.ts +++ b/test/app_test.ts @@ -1,51 +1,54 @@ -import { web3Factory } from '@0x/dev-utils'; -import { Web3ProviderEngine } from '@0x/subproviders'; +import { APIOrder, PaginatedCollection } from '@0x/connect'; 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 { setupDependenciesAsync, teardownDependenciesAsync } from './utils/deployment'; import { expect } from './utils/expect'; +import { TestCase, TestManager } from './utils/test_manager'; -let app: Express.Application; +const API_HTTP_ADDRESS = 'http://localhost:3000'; -let provider: Web3ProviderEngine; +async function apiGetRequestAsync(url: string): Promise { + return request(API_HTTP_ADDRESS).get(url); +} -describe('app test', () => { - before(async () => { - const ganacheConfigs = { - shouldUseInProcessGanache: false, - shouldAllowUnlimitedContractSize: true, - rpcUrl: config.ETHEREUM_RPC_URL, - }; - provider = web3Factory.getRpcProvider(ganacheConfigs); - await setupDependenciesAsync(); - const dependencies = await getDefaultAppDependenciesAsync(provider, config); - // start the 0x-api app - app = await getAppAsync({ ...dependencies }, config); - }); - after(async () => { - await teardownDependenciesAsync(); - // NOTE(jalextowle): The app should be torn down. I'm not going to worry - // about it because this test will be refactored so this will be a non-issue. - }); - 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([]); - }); - }); -}); +async function assertCorrectGetBodyAsync( + expectedBody: PaginatedCollection, + actualResponse: request.Response, +): Promise { + expect(actualResponse.type).to.match(/json/); + expect(actualResponse.status).to.be.eq(HttpStatus.OK); + expect(actualResponse.body).to.be.deep.eq(expectedBody); + return true; +} + +const manager = new TestManager( + { + apiGetRequestAsync, + }, + { + assertCorrectGetBodyAsync, + }, +); + +const suite: TestCase[] = [ + { + description: 'should respond to GET /sra/orders', + action: { + actionType: 'apiGetRequestAsync', + input: `${SRA_PATH}/orders`, + }, + assertion: { + assertionType: 'assertCorrectGetBodyAsync', + // This is the body of the expected HTTP request. + input: { + perPage: DEFAULT_PER_PAGE, + page: DEFAULT_PAGE, + total: 0, + records: [], + }, + }, + }, +]; +manager.executeTestSuite('app test', suite); diff --git a/test/new_app_test.ts b/test/new_app_test.ts deleted file mode 100644 index 1600aeef0..000000000 --- a/test/new_app_test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TestCase, TestManager } from './utils/test_manager'; - -const sleepActionAsync = async (time: number) => { - return new Promise(resolve => { - setTimeout(resolve, time); - }); -}; -const manager = new TestManager( - { - sleep: sleepActionAsync, - }, - { - nothing: () => true, - }, -); -const suite: TestCase[] = [ - { - description: 'sleep for 5s', - action: { - actionType: 'sleep', - input: 5000, - }, - assertion: { - assertionType: 'nothing', - expectedResult: {}, - }, - }, -]; -manager.executeTestSuite('sleep assertion', suite); diff --git a/test/utils/deployment.ts b/test/utils/deployment.ts index b8f2b4c03..00efbfbf0 100644 --- a/test/utils/deployment.ts +++ b/test/utils/deployment.ts @@ -22,18 +22,18 @@ export interface LoggingConfig { /** * Sets up a 0x-api instance. - * @param logConfig Whether or not the logs from the setup functions should + * @param loggingConfig Whether or not the logs from the setup functions should * be printed. */ -export async function setupApiAsync(logConfig: LoggingConfig = {}): Promise { +export async function setupApiAsync(loggingConfig: LoggingConfig = {}): Promise { if (yarnStartProcess) { throw new Error('Old 0x-api instance has not been torn down'); } - await setupDependenciesAsync(logConfig.shouldPrintDependencyLogs || false); + await setupDependenciesAsync(loggingConfig.shouldPrintDependencyLogs || false); yarnStartProcess = spawn('yarn', ['start'], { cwd: apiRootDir, }); - if (logConfig.shouldPrintApiLogs) { + if (loggingConfig.shouldPrintApiLogs) { yarnStartProcess.stdout.on('data', chunk => { neatlyPrintChunk('[0x-api]', chunk); }); @@ -44,20 +44,20 @@ export async function setupApiAsync(logConfig: LoggingConfig = {}): Promise { +export async function teardownApiAsync(loggingConfig: LoggingConfig = {}): Promise { if (!yarnStartProcess) { throw new Error('There is no 0x-api instance to tear down'); } yarnStartProcess.kill(); - await teardownDependenciesAsync(logConfig.shouldPrintDependencyLogs || false); + await teardownDependenciesAsync(loggingConfig.shouldPrintDependencyLogs || false); } /** diff --git a/test/utils/test_manager.ts b/test/utils/test_manager.ts index 90e107a45..fe27296d8 100644 --- a/test/utils/test_manager.ts +++ b/test/utils/test_manager.ts @@ -1,16 +1,18 @@ import 'mocha'; -import { setupApiAsync, teardownApiAsync } from './deployment'; +import { LoggingConfig, setupApiAsync, teardownApiAsync } from './deployment'; import { expect } from './expect'; // FIXME(jalextowle): Lock this down to only the actions that have been created. export type ActionType = string; // FIXME(jalextowle): Lock this down to only the assetions that have been created. +// The TestManager should remain flexible, but not when it is +// being used normally. export type AssertionType = string; // tslint:disable-next-line:interface-over-type-literal -export type ActionResult = {} | undefined; +export type ActionResult = any; export interface ActionInfo { actionType: ActionType; @@ -20,7 +22,7 @@ export interface ActionInfo { export interface AssertionInfo { assertionType: AssertionType; - expectedResult: ActionResult; + input: any; } export interface TestCase { @@ -30,12 +32,13 @@ export interface TestCase { } type Action = (input: any) => Promise; -type Assertion = (expectedResult: ActionResult, actualResult: ActionResult) => boolean; +type Assertion = (expectedResult: ActionResult, actualResult: ActionResult) => Promise; export class TestManager { constructor( protected _actionsAsync: Record, protected _assertionsAsync: Record, + protected readonly _loggingConfig?: LoggingConfig, ) {} /** @@ -56,12 +59,12 @@ export class TestManager { describe(description, () => { beforeEach(async () => { // Setup the 0x-api instance. - await setupApiAsync(); + await setupApiAsync(this._loggingConfig); }); afterEach(async () => { // Teardown the 0x-api instance. - await teardownApiAsync(); + await teardownApiAsync(this._loggingConfig); }); for (const testCase of testSuite) { @@ -91,6 +94,6 @@ export class TestManager { if (!assertionAsync) { throw new Error('[test-manager] assertion is not registered'); } - return assertionAsync(assertion.expectedResult, actualResult); + return assertionAsync(assertion.input, actualResult); } } From ea9b10965e2b9525760baa38d22b2512ca888bc1 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 15 Apr 2020 13:20:51 -0500 Subject: [PATCH 4/7] Updated `TestManager` and modularized assertions --- test/app_test.ts | 50 ++++++++++++++++++++++++-------------- test/utils/test_manager.ts | 12 ++++----- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/test/app_test.ts b/test/app_test.ts index b879862a1..c9a9a0bbe 100644 --- a/test/app_test.ts +++ b/test/app_test.ts @@ -1,4 +1,3 @@ -import { APIOrder, PaginatedCollection } from '@0x/connect'; import * as HttpStatus from 'http-status-codes'; import * as request from 'supertest'; @@ -13,14 +12,19 @@ async function apiGetRequestAsync(url: string): Promise { return request(API_HTTP_ADDRESS).get(url); } -async function assertCorrectGetBodyAsync( - expectedBody: PaginatedCollection, +async function assertResponseContentTypeAsync( + expectedContentRegex: RegExp, actualResponse: request.Response, -): Promise { - expect(actualResponse.type).to.match(/json/); - expect(actualResponse.status).to.be.eq(HttpStatus.OK); +): Promise { + expect(actualResponse.type).to.match(expectedContentRegex); +} + +async function assertResponseStatusAsync(expectedCode: number, actualResponse: request.Response): Promise { + expect(actualResponse.status).to.be.eq(expectedCode); +} + +async function assertResponseBodyAsync(expectedBody: string, actualResponse: request.Response): Promise { expect(actualResponse.body).to.be.deep.eq(expectedBody); - return true; } const manager = new TestManager( @@ -28,10 +32,11 @@ const manager = new TestManager( apiGetRequestAsync, }, { - assertCorrectGetBodyAsync, + assertResponseBodyAsync, + assertResponseStatusAsync, + assertResponseContentTypeAsync, }, ); - const suite: TestCase[] = [ { description: 'should respond to GET /sra/orders', @@ -39,16 +44,25 @@ const suite: TestCase[] = [ actionType: 'apiGetRequestAsync', input: `${SRA_PATH}/orders`, }, - assertion: { - assertionType: 'assertCorrectGetBodyAsync', - // This is the body of the expected HTTP request. - input: { - perPage: DEFAULT_PER_PAGE, - page: DEFAULT_PAGE, - total: 0, - records: [], + assertions: [ + { + assertionType: 'assertResponseStatusAsync', + input: HttpStatus.OK, }, - }, + { + assertionType: 'assertResponseContentTypeAsync', + input: /json/, + }, + { + assertionType: 'assertResponseBodyAsync', + input: { + perPage: DEFAULT_PER_PAGE, + page: DEFAULT_PAGE, + total: 0, + records: [], + }, + }, + ], }, ]; manager.executeTestSuite('app test', suite); diff --git a/test/utils/test_manager.ts b/test/utils/test_manager.ts index fe27296d8..a76e78a4a 100644 --- a/test/utils/test_manager.ts +++ b/test/utils/test_manager.ts @@ -1,7 +1,6 @@ import 'mocha'; import { LoggingConfig, setupApiAsync, teardownApiAsync } from './deployment'; -import { expect } from './expect'; // FIXME(jalextowle): Lock this down to only the actions that have been created. export type ActionType = string; @@ -28,11 +27,11 @@ export interface AssertionInfo { export interface TestCase { description: string; action: ActionInfo; - assertion: AssertionInfo; + assertions: AssertionInfo[]; } type Action = (input: any) => Promise; -type Assertion = (expectedResult: ActionResult, actualResult: ActionResult) => Promise; +type Assertion = (expectedResult: ActionResult, actualResult: ActionResult) => Promise; export class TestManager { constructor( @@ -77,8 +76,9 @@ export class TestManager { protected async _executeTestCaseAsync(testCase: TestCase): Promise { const actionResult = await this._executeActionAsync(testCase.action); - const didAssertionPass = await this._executeAssertionAsync(testCase.assertion, actionResult); - expect(didAssertionPass).to.be.true(); + for (const assertion of testCase.assertions) { + await this._executeAssertionAsync(assertion, actionResult); + } } protected async _executeActionAsync(action: ActionInfo): Promise { @@ -89,7 +89,7 @@ export class TestManager { return actionAsync(action.input); } - protected async _executeAssertionAsync(assertion: AssertionInfo, actualResult: ActionResult): Promise { + protected async _executeAssertionAsync(assertion: AssertionInfo, actualResult: ActionResult): Promise { const assertionAsync = this._assertionsAsync[assertion.assertionType]; if (!assertionAsync) { throw new Error('[test-manager] assertion is not registered'); From 9d4434c5571c7a02a975d5297536b9f55b6f703d Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 15 Apr 2020 17:03:26 -0500 Subject: [PATCH 5/7] Refactors the `TestManager` and the existing test --- test/app_test.ts | 67 ++++++++--------------- test/framework/actions.ts | 12 ++++ test/framework/assertions.ts | 17 ++++++ test/{utils => framework}/test_manager.ts | 37 +++++++++---- 4 files changed, 77 insertions(+), 56 deletions(-) create mode 100644 test/framework/actions.ts create mode 100644 test/framework/assertions.ts rename test/{utils => framework}/test_manager.ts (78%) diff --git a/test/app_test.ts b/test/app_test.ts index c9a9a0bbe..9aad0f980 100644 --- a/test/app_test.ts +++ b/test/app_test.ts @@ -1,65 +1,44 @@ import * as HttpStatus from 'http-status-codes'; -import * as request from 'supertest'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE, SRA_PATH } from '../src/constants'; -import { expect } from './utils/expect'; -import { TestCase, TestManager } from './utils/test_manager'; +import { defaultTestManager, TestCase } from './framework/test_manager'; -const API_HTTP_ADDRESS = 'http://localhost:3000'; - -async function apiGetRequestAsync(url: string): Promise { - return request(API_HTTP_ADDRESS).get(url); -} - -async function assertResponseContentTypeAsync( - expectedContentRegex: RegExp, - actualResponse: request.Response, -): Promise { - expect(actualResponse.type).to.match(expectedContentRegex); -} - -async function assertResponseStatusAsync(expectedCode: number, actualResponse: request.Response): Promise { - expect(actualResponse.status).to.be.eq(expectedCode); -} - -async function assertResponseBodyAsync(expectedBody: string, actualResponse: request.Response): Promise { - expect(actualResponse.body).to.be.deep.eq(expectedBody); -} - -const manager = new TestManager( - { - apiGetRequestAsync, - }, - { - assertResponseBodyAsync, - assertResponseStatusAsync, - assertResponseContentTypeAsync, - }, -); +const manager = defaultTestManager(); const suite: TestCase[] = [ { description: 'should respond to GET /sra/orders', action: { actionType: 'apiGetRequestAsync', - input: `${SRA_PATH}/orders`, + input: { + route: `${SRA_PATH}/orders`, + }, }, assertions: [ { - assertionType: 'assertResponseStatusAsync', - input: HttpStatus.OK, + assertionType: 'assertFieldEqualsAsync', + input: { + field: 'status', + value: HttpStatus.OK, + }, }, { - assertionType: 'assertResponseContentTypeAsync', - input: /json/, + assertionType: 'assertFieldEqualsAsync', + input: { + field: 'type', + value: /json/, + }, }, { - assertionType: 'assertResponseBodyAsync', + assertionType: 'assertFieldEqualsAsync', input: { - perPage: DEFAULT_PER_PAGE, - page: DEFAULT_PAGE, - total: 0, - records: [], + field: 'body', + value: { + perPage: DEFAULT_PER_PAGE, + page: DEFAULT_PAGE, + total: 0, + records: [], + }, }, }, ], diff --git a/test/framework/actions.ts b/test/framework/actions.ts new file mode 100644 index 000000000..1d3bbba7f --- /dev/null +++ b/test/framework/actions.ts @@ -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 apiGetRequestAsync(input: { route: string; baseURL?: string }): Promise { + return request(input.baseURL || API_HTTP_ADDRESS).get(input.route); +} diff --git a/test/framework/assertions.ts b/test/framework/assertions.ts new file mode 100644 index 000000000..f40c056eb --- /dev/null +++ b/test/framework/assertions.ts @@ -0,0 +1,17 @@ +import { expect } from '../utils/expect'; + +/** + * 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 field and the expected value of the field that + * should be compared with the actual result. + */ +export async function assertFieldEqualsAsync( + actual: any, + input: { + field: string; + value: any; + }, +): Promise { + expect(actual[input.field]).to.be.deep.eq(input.value); +} diff --git a/test/utils/test_manager.ts b/test/framework/test_manager.ts similarity index 78% rename from test/utils/test_manager.ts rename to test/framework/test_manager.ts index a76e78a4a..703afebf5 100644 --- a/test/utils/test_manager.ts +++ b/test/framework/test_manager.ts @@ -1,21 +1,34 @@ +/// FIXME(jalextowle): I have a plan for making this file type safe (with useful +/// default types). This will make writing tests significantly easier. import 'mocha'; -import { LoggingConfig, setupApiAsync, teardownApiAsync } from './deployment'; +import { LoggingConfig, setupApiAsync, teardownApiAsync } from '../utils/deployment'; + +import * as actions from './actions'; +import * as assertions from './assertions'; + +/** + * Constructs the default `TestManager` class. + * @returns TestManager Returns a `TestManager` that has been given default actions + * and assertions. + */ +export function defaultTestManager(): TestManager { + return new TestManager( + { + apiGetRequestAsync: actions.apiGetRequestAsync, + }, + { + assertFieldEqualsAsync: assertions.assertFieldEqualsAsync, + }, + ); +} -// FIXME(jalextowle): Lock this down to only the actions that have been created. export type ActionType = string; -// FIXME(jalextowle): Lock this down to only the assetions that have been created. -// The TestManager should remain flexible, but not when it is -// being used normally. export type AssertionType = string; -// tslint:disable-next-line:interface-over-type-literal -export type ActionResult = any; - export interface ActionInfo { actionType: ActionType; - // FIXME(jalextowle): Lock this down to only the actions that have been created. input: any; } @@ -31,7 +44,7 @@ export interface TestCase { } type Action = (input: any) => Promise; -type Assertion = (expectedResult: ActionResult, actualResult: ActionResult) => Promise; +type Assertion = (actualResult: any, input: any) => Promise; export class TestManager { constructor( @@ -81,7 +94,7 @@ export class TestManager { } } - protected async _executeActionAsync(action: ActionInfo): Promise { + protected async _executeActionAsync(action: ActionInfo): Promise { const actionAsync = this._actionsAsync[action.actionType]; if (!actionAsync) { throw new Error('[test-manager] action is not registered'); @@ -89,7 +102,7 @@ export class TestManager { return actionAsync(action.input); } - protected async _executeAssertionAsync(assertion: AssertionInfo, actualResult: ActionResult): Promise { + protected async _executeAssertionAsync(assertion: AssertionInfo, actualResult: any): Promise { const assertionAsync = this._assertionsAsync[assertion.assertionType]; if (!assertionAsync) { throw new Error('[test-manager] assertion is not registered'); From 385daff341d119d3038e54b3c696e68ad5bf0b37 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 16 Apr 2020 13:11:58 -0500 Subject: [PATCH 6/7] Refactored the framework and added a strict module --- package.json | 2 + test/app_test.ts | 14 +++---- test/framework/actions.ts | 2 +- test/framework/assertions.ts | 2 +- test/framework/strict_utils.ts | 32 ++++++++++++++++ test/framework/test_manager.ts | 67 ++++++++++++++-------------------- test/utils/deployment.ts | 8 +++- yarn.lock | 31 +++++++++++++++- 8 files changed, 108 insertions(+), 50 deletions(-) create mode 100644 test/framework/strict_utils.ts diff --git a/package.json b/package.json index 856d2e2d6..aaae9399d 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/test/app_test.ts b/test/app_test.ts index 9aad0f980..9ff559649 100644 --- a/test/app_test.ts +++ b/test/app_test.ts @@ -2,35 +2,35 @@ import * as HttpStatus from 'http-status-codes'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE, SRA_PATH } from '../src/constants'; -import { defaultTestManager, TestCase } from './framework/test_manager'; +import { STRICT_ACTIONS, STRICT_ASSERTIONS, StrictTestCaseType, strictTestManager } from './framework/strict_utils'; -const manager = defaultTestManager(); -const suite: TestCase[] = [ +const manager = strictTestManager(); +const suite: StrictTestCaseType[] = [ { description: 'should respond to GET /sra/orders', action: { - actionType: 'apiGetRequestAsync', + actionType: STRICT_ACTIONS.HTTP_GET, input: { route: `${SRA_PATH}/orders`, }, }, assertions: [ { - assertionType: 'assertFieldEqualsAsync', + assertionType: STRICT_ASSERTIONS.EQUALS, input: { field: 'status', value: HttpStatus.OK, }, }, { - assertionType: 'assertFieldEqualsAsync', + assertionType: STRICT_ASSERTIONS.EQUALS, input: { field: 'type', value: /json/, }, }, { - assertionType: 'assertFieldEqualsAsync', + assertionType: STRICT_ASSERTIONS.EQUALS, input: { field: 'body', value: { diff --git a/test/framework/actions.ts b/test/framework/actions.ts index 1d3bbba7f..1e64113d0 100644 --- a/test/framework/actions.ts +++ b/test/framework/actions.ts @@ -7,6 +7,6 @@ const API_HTTP_ADDRESS = 'http://localhost:3000'; * @param input Specifies the route and the base URL that should be used to make * the HTTP GET request. */ -export async function apiGetRequestAsync(input: { route: string; baseURL?: string }): Promise { +export async function httpGetAsync(input: { route: string; baseURL?: string }): Promise { return request(input.baseURL || API_HTTP_ADDRESS).get(input.route); } diff --git a/test/framework/assertions.ts b/test/framework/assertions.ts index f40c056eb..29a82c08d 100644 --- a/test/framework/assertions.ts +++ b/test/framework/assertions.ts @@ -6,7 +6,7 @@ import { expect } from '../utils/expect'; * @param input Specifies both the field and the expected value of the field that * should be compared with the actual result. */ -export async function assertFieldEqualsAsync( +export async function assertEqualsAsync( actual: any, input: { field: string; diff --git a/test/framework/strict_utils.ts b/test/framework/strict_utils.ts new file mode 100644 index 000000000..53ea7369d --- /dev/null +++ b/test/framework/strict_utils.ts @@ -0,0 +1,32 @@ +import * as actions from './actions'; +import * as assertions from './assertions'; +import { TestCase, TestManager } from './test_manager'; + +export const STRICT_ACTIONS = { + HTTP_GET: 'httpGetAsync' as StrictActionType, +}; + +export const STRICT_ASSERTIONS = { + EQUALS: 'assertEqualsAsync' as StrictAssertionType, +}; + +export type StrictActionType = 'httpGetAsync'; +export type StrictAssertionType = 'assertEqualsAsync'; +export type StrictTestCaseType = TestCase; + +/** + * 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 strictTestManager(): TestManager { + return new TestManager( + { + httpGetAsync: actions.httpGetAsync, + }, + { + assertEqualsAsync: assertions.assertEqualsAsync, + }, + ); +} diff --git a/test/framework/test_manager.ts b/test/framework/test_manager.ts index 703afebf5..e4b5c124e 100644 --- a/test/framework/test_manager.ts +++ b/test/framework/test_manager.ts @@ -4,52 +4,33 @@ import 'mocha'; import { LoggingConfig, setupApiAsync, teardownApiAsync } from '../utils/deployment'; -import * as actions from './actions'; -import * as assertions from './assertions'; - -/** - * Constructs the default `TestManager` class. - * @returns TestManager Returns a `TestManager` that has been given default actions - * and assertions. - */ -export function defaultTestManager(): TestManager { - return new TestManager( - { - apiGetRequestAsync: actions.apiGetRequestAsync, - }, - { - assertFieldEqualsAsync: assertions.assertFieldEqualsAsync, - }, - ); +export interface ActionInfo { + actionType: TType; + input?: any; + inputPath?: string; + outputPath?: string; } -export type ActionType = string; - -export type AssertionType = string; - -export interface ActionInfo { - actionType: ActionType; +export interface AssertionInfo { + assertionType: TType; input: any; } -export interface AssertionInfo { - assertionType: AssertionType; - input: any; -} - -export interface TestCase { +export interface TestCase { description: string; - action: ActionInfo; - assertions: AssertionInfo[]; + action: ActionInfo; + assertions: Array>; } type Action = (input: any) => Promise; type Assertion = (actualResult: any, input: any) => Promise; -export class TestManager { +export class TestManager { + protected _data: any = {}; + constructor( - protected _actionsAsync: Record, - protected _assertionsAsync: Record, + protected _actionsAsync: Record, + protected _assertionsAsync: Record, protected readonly _loggingConfig?: LoggingConfig, ) {} @@ -62,7 +43,7 @@ export class TestManager { * @param testSuite The set of test cases that should be executed (in order) * in this suite. */ - public executeTestSuite(description: string, testSuite: TestCase[]): void { + public executeTestSuite(description: string, testSuite: Array>): void { if (!testSuite.length) { throw new Error(`[test-manager] suite '${description}' is empty`); } @@ -87,22 +68,30 @@ export class TestManager { }); } - protected async _executeTestCaseAsync(testCase: TestCase): Promise { + protected async _executeTestCaseAsync(testCase: TestCase): Promise { const actionResult = await this._executeActionAsync(testCase.action); for (const assertion of testCase.assertions) { await this._executeAssertionAsync(assertion, actionResult); } } - protected async _executeActionAsync(action: ActionInfo): Promise { + protected async _executeActionAsync(action: ActionInfo): Promise { const actionAsync = this._actionsAsync[action.actionType]; if (!actionAsync) { throw new Error('[test-manager] action is not registered'); } - return actionAsync(action.input); + 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, actualResult: any): Promise { + protected async _executeAssertionAsync(assertion: AssertionInfo, actualResult: any): Promise { const assertionAsync = this._assertionsAsync[assertion.assertionType]; if (!assertionAsync) { throw new Error('[test-manager] assertion is not registered'); diff --git a/test/utils/deployment.ts b/test/utils/deployment.ts index 00efbfbf0..7813d5cc0 100644 --- a/test/utils/deployment.ts +++ b/test/utils/deployment.ts @@ -1,8 +1,11 @@ import { logUtils as log } from '@0x/utils'; import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; import { resolve as resolvePath } from 'path'; +import * as rimraf from 'rimraf'; +import { promisify } from 'util'; const apiRootDir = resolvePath(`${__dirname}/../../../`); +const rimrafAsync = promisify(rimraf); let yarnStartProcess: ChildProcessWithoutNullStreams; @@ -85,7 +88,7 @@ export async function setupDependenciesAsync(shouldPrintLogs: boolean = false): // Wait for the dependencies to boot up. // HACK(jalextowle): This should really be replaced by log-scraping, but it // does appear to work for now. - await sleepAsync(10000); // tslint:disable-line:custom-no-magic-numbers + await sleepAsync(17500); // tslint:disable-line:custom-no-magic-numbers } /** @@ -105,6 +108,9 @@ export async function teardownDependenciesAsync(shouldPrintLogs: boolean = false neatlyPrintChunk('[docker-compose down | error]', chunk); }); } + // TODO(jalextowle): Add archival logic. + await rimrafAsync(`${apiRootDir}/0x_mesh`); + await rimrafAsync(`${apiRootDir}/postgres`); } function neatlyPrintChunk(prefix: string, chunk: Buffer): void { diff --git a/yarn.lock b/yarn.lock index 549a64cb0..102b7a5a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1030,6 +1030,11 @@ dependencies: bignumber.js "7.2.1" +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + "@types/express-serve-static-core@*": version "4.16.9" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz#69e00643b0819b024bdede95ced3ff239bb54558" @@ -1045,6 +1050,15 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" +"@types/glob@*": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + "@types/hdkey@^0.7.0": version "0.7.1" resolved "https://registry.yarnpkg.com/@types/hdkey/-/hdkey-0.7.1.tgz#9bc63ebbe96b107b277b65ea7a95442a677d0d61" @@ -1067,7 +1081,7 @@ version "2.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" -"@types/minimatch@^3.0.3": +"@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -1118,6 +1132,14 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/rimraf@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.0.tgz#b9d03f090ece263671898d57bb7bb007023ac19f" + integrity sha512-7WhJ0MdpFgYQPXlF4Dx+DhgvlPCfz/x5mHaeDQAKhcenvQP1KCpLQ18JklAqeGMYSAT2PxLpzd0g2/HE7fj7hQ== + dependencies: + "@types/glob" "*" + "@types/node" "*" + "@types/serve-static@*": version "1.13.3" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -6988,6 +7010,13 @@ rimraf@^2.2.8, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" From 9d7f7977a0d7421c5b7e1bcee2ba5762a8cdf77f Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 16 Apr 2020 13:29:12 -0500 Subject: [PATCH 7/7] Addressed review feedback --- test/app_test.ts | 20 ++++++------- test/framework/assertions.ts | 30 ++++++++++++++++---- test/framework/{strict_utils.ts => index.ts} | 18 ++++++------ test/framework/test_manager.ts | 8 ++---- 4 files changed, 48 insertions(+), 28 deletions(-) rename test/framework/{strict_utils.ts => index.ts} (51%) diff --git a/test/app_test.ts b/test/app_test.ts index 9ff559649..e5def5514 100644 --- a/test/app_test.ts +++ b/test/app_test.ts @@ -2,37 +2,37 @@ import * as HttpStatus from 'http-status-codes'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE, SRA_PATH } from '../src/constants'; -import { STRICT_ACTIONS, STRICT_ASSERTIONS, StrictTestCaseType, strictTestManager } from './framework/strict_utils'; +import { ACTIONS, ASSERTIONS, createTestManager, TestCaseType } from './framework'; -const manager = strictTestManager(); -const suite: StrictTestCaseType[] = [ +const manager = createTestManager(); +const suite: TestCaseType[] = [ { description: 'should respond to GET /sra/orders', action: { - actionType: STRICT_ACTIONS.HTTP_GET, + actionType: ACTIONS.HTTP_GET, input: { route: `${SRA_PATH}/orders`, }, }, assertions: [ { - assertionType: STRICT_ASSERTIONS.EQUALS, + assertionType: ASSERTIONS.EQUALS, input: { - field: 'status', + path: 'status', value: HttpStatus.OK, }, }, { - assertionType: STRICT_ASSERTIONS.EQUALS, + assertionType: ASSERTIONS.MATCHES, input: { - field: 'type', + path: 'type', value: /json/, }, }, { - assertionType: STRICT_ASSERTIONS.EQUALS, + assertionType: ASSERTIONS.EQUALS, input: { - field: 'body', + path: 'body', value: { perPage: DEFAULT_PER_PAGE, page: DEFAULT_PAGE, diff --git a/test/framework/assertions.ts b/test/framework/assertions.ts index 29a82c08d..da28288a6 100644 --- a/test/framework/assertions.ts +++ b/test/framework/assertions.ts @@ -1,17 +1,37 @@ import { expect } from '../utils/expect'; /** - * Asserts that a specified field of the actual result is equal to an expected value. + * 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 field and the expected value of the field that - * should be compared with the actual result. + * @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: { - field: string; + path?: string; value: any; }, ): Promise { - expect(actual[input.field]).to.be.deep.eq(input.value); + 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 { + const actual_ = input.path ? actual[input.path] : actual; + expect(actual_).to.match(input.value); } diff --git a/test/framework/strict_utils.ts b/test/framework/index.ts similarity index 51% rename from test/framework/strict_utils.ts rename to test/framework/index.ts index 53ea7369d..b23690089 100644 --- a/test/framework/strict_utils.ts +++ b/test/framework/index.ts @@ -2,17 +2,18 @@ import * as actions from './actions'; import * as assertions from './assertions'; import { TestCase, TestManager } from './test_manager'; -export const STRICT_ACTIONS = { - HTTP_GET: 'httpGetAsync' as StrictActionType, +export const ACTIONS = { + HTTP_GET: 'httpGetAsync' as ActionType, }; -export const STRICT_ASSERTIONS = { - EQUALS: 'assertEqualsAsync' as StrictAssertionType, +export const ASSERTIONS = { + EQUALS: 'assertEqualsAsync' as AssertionType, + MATCHES: 'assertMatchesAsync' as AssertionType, }; -export type StrictActionType = 'httpGetAsync'; -export type StrictAssertionType = 'assertEqualsAsync'; -export type StrictTestCaseType = TestCase; +export type ActionType = 'httpGetAsync'; +export type AssertionType = 'assertEqualsAsync' | 'assertMatchesAsync'; +export type TestCaseType = TestCase; /** * Constructs the default `TestManager` class, which uses strict type declarations @@ -20,13 +21,14 @@ export type StrictTestCaseType = TestCase * @returns TestManager Returns a `TestManager` that has been given default actions * and assertions. */ -export function strictTestManager(): TestManager { +export function createTestManager(): TestManager { return new TestManager( { httpGetAsync: actions.httpGetAsync, }, { assertEqualsAsync: assertions.assertEqualsAsync, + assertMatchesAsync: assertions.assertMatchesAsync, }, ); } diff --git a/test/framework/test_manager.ts b/test/framework/test_manager.ts index e4b5c124e..260c7c79d 100644 --- a/test/framework/test_manager.ts +++ b/test/framework/test_manager.ts @@ -1,5 +1,3 @@ -/// FIXME(jalextowle): I have a plan for making this file type safe (with useful -/// default types). This will make writing tests significantly easier. import 'mocha'; import { LoggingConfig, setupApiAsync, teardownApiAsync } from '../utils/deployment'; @@ -78,7 +76,7 @@ export class TestManager): Promise { const actionAsync = this._actionsAsync[action.actionType]; if (!actionAsync) { - throw new Error('[test-manager] action is not registered'); + throw new Error(`[test-manager] action ${action.actionType} is not registered`); } const input = action.inputPath ? this._data[action.inputPath] : {}; const result = await actionAsync({ @@ -94,8 +92,8 @@ export class TestManager, actualResult: any): Promise { const assertionAsync = this._assertionsAsync[assertion.assertionType]; if (!assertionAsync) { - throw new Error('[test-manager] assertion is not registered'); + throw new Error(`[test-manager] assertion ${assertion.assertionType} is not registered`); } - return assertionAsync(assertion.input, actualResult); + return assertionAsync(actualResult, assertion.input); } }