From 3d2463d834e7cac6a80d88e71eebf68a0f1acfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 20 Sep 2018 10:17:06 +0200 Subject: [PATCH] feat(testlab): add createRestAppClient(), simplify usage in tests Clean up all test code (including examples in the documentation) to use `Client` instead of `supertest.SuperTest`. Remove `createClientForRestServer` because it was not used anywhere and had the issue of not stopping the server after the test is done. Add a new helper `createRestAppClient` instead, rework acceptance tests (including the templates) to use this new helper. --- docs/site/Implementing-features.shelved.md | 16 ++++---- docs/site/Testing-your-application.md | 12 +++--- .../test/acceptance/application.acceptance.ts | 12 ++---- .../test/acceptance/application.acceptance.ts | 7 ++-- .../acceptance/todo-list-todo.acceptance.ts | 8 ++-- .../test/acceptance/todo-list.acceptance.ts | 6 +-- .../test/acceptance/todo.acceptance.ts | 6 +-- .../todo/test/acceptance/todo.acceptance.ts | 6 +-- .../controller.booter.acceptance.ts | 14 ++++--- .../ping.controller.acceptance.ts.ejs | 14 ++----- .../bootstrapping/rest.acceptance.ts | 2 +- .../coercion/coercion.acceptance.ts | 15 ++++--- .../validation/validation.acceptance.ts | 14 ++++--- packages/testlab/README.md | 29 ++++++++++++++ packages/testlab/src/client.ts | 40 +++++++++++-------- 15 files changed, 119 insertions(+), 82 deletions(-) diff --git a/docs/site/Implementing-features.shelved.md b/docs/site/Implementing-features.shelved.md index 32a154ccc983..901fa0eb3fcc 100644 --- a/docs/site/Implementing-features.shelved.md +++ b/docs/site/Implementing-features.shelved.md @@ -40,11 +40,11 @@ Create `test/acceptance/product.acceptance.ts` with the following contents: ```ts import {HelloWorldApp} from '../..'; import {RestBindings, RestServer} from '@loopback/rest'; -import {expect, supertest} from '@loopback/testlab'; +import {Client, expect, supertest} from '@loopback/testlab'; describe('Product (acceptance)', () => { let app: HelloWorldApp; - let request: supertest.SuperTest; + let request: Client; before(givenEmptyDatabase); before(givenRunningApp); @@ -84,13 +84,13 @@ describe('Product (acceptance)', () => { } async function givenRunningApp() { - app = new HelloWorldApp(); - const server = await app.getServer(RestServer); - server.bind(RestBindings.PORT).to(0); + app = new HelloWorldApp({ + rest: { + port: 0, + }, + }); await app.start(); - - const port: number = await server.get(RestBindings.PORT); - request = supertest(`http://127.0.0.1:${port}`); + request = supertest(app.restServer.url); } async function givenProduct(data: Object) { diff --git a/docs/site/Testing-your-application.md b/docs/site/Testing-your-application.md index b6751418d14c..37834d244925 100644 --- a/docs/site/Testing-your-application.md +++ b/docs/site/Testing-your-application.md @@ -833,7 +833,7 @@ Here is an example of an acceptance test: ```ts import {HelloWorldApplication} from '../..'; -import {expect, createClientForHandler, Client} from '@loopback/testlab'; +import {Client, createRestAppClient, expect} from '@loopback/testlab'; import {givenEmptyDatabase, givenProduct} from '../helpers/database.helpers'; import {RestServer, RestBindings} from '@loopback/rest'; import {testdb} from '../fixtures/datasources/testdb.datasource'; @@ -870,14 +870,16 @@ describe('Product (acceptance)', () => { }); async function givenRunningApp() { - app = new HelloWorldApplication(); + app = new HelloWorldApplication({ + rest: { + port: 0, + }, + }); app.dataSource(testdb); - const server = await app.getServer(RestServer); - server.bind(RestBindings.PORT).to(0); await app.boot(); await app.start(); - client = createClientForHandler(server.handleHttp); + client = createRestAppClient(app); } }); ``` diff --git a/examples/hello-world/test/acceptance/application.acceptance.ts b/examples/hello-world/test/acceptance/application.acceptance.ts index 86c5b1f23272..def9643db2fa 100644 --- a/examples/hello-world/test/acceptance/application.acceptance.ts +++ b/examples/hello-world/test/acceptance/application.acceptance.ts @@ -3,23 +3,17 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {createClientForHandler, expect, supertest} from '@loopback/testlab'; -import {RestServer} from '@loopback/rest'; +import {Client, createRestAppClient, expect} from '@loopback/testlab'; import {HelloWorldApplication} from '../../src/application'; describe('Application', () => { let app: HelloWorldApplication; - let client: supertest.SuperTest; - let server: RestServer; + let client: Client; before(givenAnApplication); before(async () => { await app.start(); - server = await app.getServer(RestServer); - }); - - before(() => { - client = createClientForHandler(server.requestHandler); + client = createRestAppClient(app); }); after(async () => { await app.stop(); diff --git a/examples/soap-calculator/test/acceptance/application.acceptance.ts b/examples/soap-calculator/test/acceptance/application.acceptance.ts index a1f1707352e0..9cfb325ba0f6 100644 --- a/examples/soap-calculator/test/acceptance/application.acceptance.ts +++ b/examples/soap-calculator/test/acceptance/application.acceptance.ts @@ -1,10 +1,9 @@ -import {supertest} from '@loopback/testlab'; +import {Client, createRestAppClient, expect} from '@loopback/testlab'; import {SoapCalculatorApplication} from '../../src/application'; -import {expect} from '@loopback/testlab'; describe('Application', function() { let app: SoapCalculatorApplication; - let client: supertest.SuperTest; + let client: Client; // tslint:disable-next-line:no-invalid-this this.timeout(30000); @@ -14,7 +13,7 @@ describe('Application', function() { before(async () => { await app.boot(); await app.start(); - client = supertest(app.restServer.url); + client = createRestAppClient(app); }); after(async () => { diff --git a/examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts b/examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts index bdb0e49a06da..8d9a34484409 100644 --- a/examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts +++ b/examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts @@ -3,15 +3,15 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {createClientForHandler, expect, supertest} from '@loopback/testlab'; +import {Client, createRestAppClient, expect} from '@loopback/testlab'; import {TodoListApplication} from '../../src/application'; import {Todo, TodoList} from '../../src/models/'; -import {TodoRepository, TodoListRepository} from '../../src/repositories/'; +import {TodoListRepository, TodoRepository} from '../../src/repositories/'; import {givenTodo, givenTodoList} from '../helpers'; describe('TodoListApplication', () => { let app: TodoListApplication; - let client: supertest.SuperTest; + let client: Client; let todoRepo: TodoRepository; let todoListRepo: TodoListRepository; @@ -23,7 +23,7 @@ describe('TodoListApplication', () => { before(givenTodoRepository); before(givenTodoListRepository); before(() => { - client = createClientForHandler(app.requestHandler); + client = createRestAppClient(app); }); beforeEach(async () => { diff --git a/examples/todo-list/test/acceptance/todo-list.acceptance.ts b/examples/todo-list/test/acceptance/todo-list.acceptance.ts index bf9df2adf3bc..79d33f632748 100644 --- a/examples/todo-list/test/acceptance/todo-list.acceptance.ts +++ b/examples/todo-list/test/acceptance/todo-list.acceptance.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {EntityNotFoundError} from '@loopback/repository'; -import {createClientForHandler, expect, supertest} from '@loopback/testlab'; +import {Client, createRestAppClient, expect} from '@loopback/testlab'; import {TodoListApplication} from '../../src/application'; import {TodoList} from '../../src/models/'; import {TodoListRepository} from '../../src/repositories/'; @@ -12,7 +12,7 @@ import {givenTodoList} from '../helpers'; describe('TodoListApplication', () => { let app: TodoListApplication; - let client: supertest.SuperTest; + let client: Client; let todoListRepo: TodoListRepository; before(givenRunningApplicationWithCustomConfiguration); @@ -20,7 +20,7 @@ describe('TodoListApplication', () => { before(givenTodoListRepository); before(() => { - client = createClientForHandler(app.requestHandler); + client = createRestAppClient(app); }); beforeEach(async () => { diff --git a/examples/todo-list/test/acceptance/todo.acceptance.ts b/examples/todo-list/test/acceptance/todo.acceptance.ts index e809587f55f1..9bf42b3f3f93 100644 --- a/examples/todo-list/test/acceptance/todo.acceptance.ts +++ b/examples/todo-list/test/acceptance/todo.acceptance.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {EntityNotFoundError} from '@loopback/repository'; -import {createClientForHandler, expect, supertest} from '@loopback/testlab'; +import {Client, createRestAppClient, expect} from '@loopback/testlab'; import {TodoListApplication} from '../../src/application'; import {Todo} from '../../src/models/'; import {TodoRepository} from '../../src/repositories/'; @@ -12,7 +12,7 @@ import {givenTodo} from '../helpers'; describe('TodoListApplication', () => { let app: TodoListApplication; - let client: supertest.SuperTest; + let client: Client; let todoRepo: TodoRepository; before(givenRunningApplicationWithCustomConfiguration); @@ -20,7 +20,7 @@ describe('TodoListApplication', () => { before(givenTodoRepository); before(() => { - client = createClientForHandler(app.requestHandler); + client = createRestAppClient(app); }); beforeEach(async () => { diff --git a/examples/todo/test/acceptance/todo.acceptance.ts b/examples/todo/test/acceptance/todo.acceptance.ts index 7a383dde8799..76723a1fb8a4 100644 --- a/examples/todo/test/acceptance/todo.acceptance.ts +++ b/examples/todo/test/acceptance/todo.acceptance.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {EntityNotFoundError} from '@loopback/repository'; -import {createClientForHandler, expect, supertest} from '@loopback/testlab'; +import {Client, createRestAppClient, expect} from '@loopback/testlab'; import {TodoListApplication} from '../../src/application'; import {Todo} from '../../src/models/'; import {TodoRepository} from '../../src/repositories/'; @@ -18,7 +18,7 @@ import { describe('TodoApplication', () => { let app: TodoListApplication; - let client: supertest.SuperTest; + let client: Client; let todoRepo: TodoRepository; let cachingProxy: HttpCachingProxy; @@ -30,7 +30,7 @@ describe('TodoApplication', () => { before(givenTodoRepository); before(() => { - client = createClientForHandler(app.requestHandler); + client = createRestAppClient(app); }); beforeEach(async () => { diff --git a/packages/boot/test/acceptance/controller.booter.acceptance.ts b/packages/boot/test/acceptance/controller.booter.acceptance.ts index 21b752e7ecce..b30db3c3a421 100644 --- a/packages/boot/test/acceptance/controller.booter.acceptance.ts +++ b/packages/boot/test/acceptance/controller.booter.acceptance.ts @@ -3,8 +3,11 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Client, createClientForHandler, TestSandbox} from '@loopback/testlab'; -import {RestServer} from '@loopback/rest'; +import { + createRestAppClient, + givenHttpServerConfig, + TestSandbox, +} from '@loopback/testlab'; import {resolve} from 'path'; import {BooterApp} from '../fixtures/application'; @@ -22,8 +25,7 @@ describe('controller booter acceptance tests', () => { await app.boot(); await app.start(); - const server: RestServer = await app.getServer(RestServer); - const client: Client = createClientForHandler(server.requestHandler); + const client = createRestAppClient(app); // Default Controllers = /controllers with .controller.js ending (nested = true); await client.get('/one').expect(200, 'ControllerOne.one()'); @@ -38,7 +40,9 @@ describe('controller booter acceptance tests', () => { ); const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp; - app = new MyApp(); + app = new MyApp({ + rest: givenHttpServerConfig({port: 0}), + }); } async function stopApp() { diff --git a/packages/cli/generators/app/templates/test/acceptance/ping.controller.acceptance.ts.ejs b/packages/cli/generators/app/templates/test/acceptance/ping.controller.acceptance.ts.ejs index 1dd896096f5d..547532b175cf 100644 --- a/packages/cli/generators/app/templates/test/acceptance/ping.controller.acceptance.ts.ejs +++ b/packages/cli/generators/app/templates/test/acceptance/ping.controller.acceptance.ts.ejs @@ -1,23 +1,19 @@ -import {createClientForHandler, supertest} from '@loopback/testlab'; -import {RestServer} from '@loopback/rest'; +import {Client, createRestAppClient} from '@loopback/testlab'; import {<%= project.applicationName %>} from '../..'; describe('PingController', () => { let app: <%= project.applicationName %>; - let server: RestServer; - let client: supertest.SuperTest; + let client: Client; before(givenAnApplication); - before(givenARestServer); - before(async () => { await app.boot(); await app.start(); }); before(() => { - client = createClientForHandler(server.requestHandler); + client = createRestAppClient(app); }); after(async () => { @@ -35,8 +31,4 @@ describe('PingController', () => { }, }); } - - async function givenARestServer() { - server = await app.getServer(RestServer); - } }); diff --git a/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts b/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts index 002a041cf1e6..d5f71f2fa194 100644 --- a/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts +++ b/packages/rest/test/acceptance/bootstrapping/rest.acceptance.ts @@ -56,7 +56,7 @@ describe('Starting the application', () => { it('starts an HTTP server (using RestApplication)', async () => { const app = new RestApplication(); - app.bind(RestBindings.PORT).to(0); + app.restServer.bind(RestBindings.PORT).to(0); app.handler(sequenceHandler); await startServerCheck(app); }); diff --git a/packages/rest/test/acceptance/coercion/coercion.acceptance.ts b/packages/rest/test/acceptance/coercion/coercion.acceptance.ts index 87bdfef8abbd..c0a53e5969a2 100644 --- a/packages/rest/test/acceptance/coercion/coercion.acceptance.ts +++ b/packages/rest/test/acceptance/coercion/coercion.acceptance.ts @@ -1,9 +1,14 @@ -import {supertest, createClientForHandler, sinon} from '@loopback/testlab'; -import {RestApplication, get, param} from '../../..'; +import { + Client, + createRestAppClient, + givenHttpServerConfig, + sinon, +} from '@loopback/testlab'; +import {get, param, RestApplication} from '../../..'; describe('Coercion', () => { let app: RestApplication; - let client: supertest.SuperTest; + let client: Client; let spy: sinon.SinonSpy; before(givenAClient); @@ -91,9 +96,9 @@ describe('Coercion', () => { }); async function givenAClient() { - app = new RestApplication(); + app = new RestApplication({rest: givenHttpServerConfig({port: 0})}); app.controller(MyController); await app.start(); - client = createClientForHandler(app.requestHandler); + client = createRestAppClient(app); } }); diff --git a/packages/rest/test/acceptance/validation/validation.acceptance.ts b/packages/rest/test/acceptance/validation/validation.acceptance.ts index e596d6bd21b1..a50f4fd3d4ef 100644 --- a/packages/rest/test/acceptance/validation/validation.acceptance.ts +++ b/packages/rest/test/acceptance/validation/validation.acceptance.ts @@ -5,20 +5,24 @@ import {ControllerClass} from '@loopback/core'; import {model, property} from '@loopback/repository'; -import {createClientForHandler, supertest} from '@loopback/testlab'; import { - RestApplication, + Client, + createRestAppClient, + givenHttpServerConfig, +} from '@loopback/testlab'; +import { api, getJsonSchema, jsonToSchemaObject, post, requestBody, + RestApplication, } from '../../..'; import {aBodySpec} from '../../helpers'; describe('Validation at REST level', () => { let app: RestApplication; - let client: supertest.SuperTest; + let client: Client; @model() class Product { @@ -152,10 +156,10 @@ describe('Validation at REST level', () => { } async function givenAnAppAndAClient(controller: ControllerClass) { - app = new RestApplication(); + app = new RestApplication({rest: givenHttpServerConfig({port: 0})}); app.controller(controller); await app.start(); - client = createClientForHandler(app.requestHandler); + client = createRestAppClient(app); } }); diff --git a/packages/testlab/README.md b/packages/testlab/README.md index 087de912395d..35211c6cc11e 100644 --- a/packages/testlab/README.md +++ b/packages/testlab/README.md @@ -48,6 +48,8 @@ Table of contents: - [shot](#shot) - HTTP Request/Response stubs. - [validateApiSpec](#validateapispec) - Open API Spec validator. - [itSkippedOnTravis](#itskippedontravis) - Skip tests on Travis env. +- [createRestAppClient](#createrestappclient) - Create a supertest client + connected to a running RestApplication. - [givenHttpServerConfig](#givenhttpserverconfig) - Generate HTTP server config. - [httpGetAsync](#httpgetasync) - Async wrapper for HTTP GET requests. - [httpsGetAsync](#httpsgetasync) - Async wrapper for HTTPS GET requests. @@ -82,6 +84,33 @@ by Shot in your unit tests: Helper function for skipping tests on Travis environment. If you need to skip testing on Travis for any reason, use this instead of Mocha's `it`. +### `createRestAppClient` + +Helper function to create a `supertest` client connected to a running +RestApplication. It is the responsibility of the caller to ensure that the app +is running and to stop the application after all tests are done. + +Example use: + +```ts +import {Client, createRestAppClient} from '@loopback/testlab'; + +describe('My application', () => { + app: MyApplication; // extends RestApplication + client: Client; + + before(givenRunningApplication); + before(() => { + client = createRestAppClient(app); + }); + after(() => app.stop()); + + it('invokes GET /ping', async () => { + await client.get('/ping?msg=world').expect(200); + }); +}); +``` + ### `givenhttpserverconfig` Helper function for generating Travis-friendly host (127.0.0.1). This is diff --git a/packages/testlab/src/client.ts b/packages/testlab/src/client.ts index 1e97f0788687..acedaf7ddc89 100644 --- a/packages/testlab/src/client.ts +++ b/packages/testlab/src/client.ts @@ -26,23 +26,31 @@ export function createClientForHandler( return supertest(server); } -export async function createClientForRestServer( - server: RestServer, -): Promise { - await server.start(); - const port = - server.options && server.options.http ? server.options.http.port : 3000; - const url = `http://127.0.0.1:${port}`; - // TODO(bajtos) Find a way how to stop the server after all tests are done +/** + * Create a SuperTest client for a running RestApplication instance. + * It is the responsibility of the caller to ensure that the app + * is running and to stop the application after all tests are done. + * @param app A running (listening) instance of a RestApplication. + */ +export function createRestAppClient(app: RestApplicationLike) { + const url = app.restServer.url; + if (!url) { + throw new Error( + `Cannot create client for ${app.constructor.name}, it is not listening.`, + ); + } return supertest(url); } -// These interfaces are meant to partially mirror the formats provided -// in our other libraries to avoid circular imports. -export interface RestServer { - start(): Promise; - options?: { - // tslint:disable-next-line:no-any - [prop: string]: any; - }; +/* + * These interfaces are meant to partially mirror the formats provided + * in our other libraries to avoid circular imports. + */ + +export interface RestApplicationLike { + restServer: RestServerLike; +} + +export interface RestServerLike { + url?: string; }