From 8a9ab2ad036d9da0764b570d7b105f1fe697bf76 Mon Sep 17 00:00:00 2001 From: "oskar.dudycz" Date: Fri, 19 Mar 2021 14:22:08 +0100 Subject: [PATCH] Added API tests with SuperTest: - extracted app into separate file to enable SuperTest usage, - added the sample API test for the default route, - added separate NPM script for API tests and general, - updated CI config to run all tests. --- .github/workflows/samples_simple.yml | 2 +- README.md | 109 +++++++++++- samples/simple/package-lock.json | 162 +++++++++++++++++- samples/simple/package.json | 6 +- samples/simple/src/app.ts | 10 ++ .../src/greetings/getGreetings.api.test.ts | 11 ++ samples/simple/src/index.ts | 8 +- 7 files changed, 292 insertions(+), 16 deletions(-) create mode 100644 samples/simple/src/app.ts create mode 100644 samples/simple/src/greetings/getGreetings.api.test.ts diff --git a/.github/workflows/samples_simple.yml b/.github/workflows/samples_simple.yml index 447d512a..5edc81ab 100644 --- a/.github/workflows/samples_simple.yml +++ b/.github/workflows/samples_simple.yml @@ -39,4 +39,4 @@ jobs: # run build - run: npm run build:ts # run tests - - run: npm run test:unit + - run: npm test diff --git a/README.md b/README.md index 71d0c644..9dc9a7ea 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - [General configuration](#general-configuration) - [VSCode debug configuration](#vscode-debug-configuration) - [Unit tests with Jest](#unit-tests-with-jest) + - [API tests with SuperTest](#api-tests-with-supertest) - [CI - Run tests with Github Actions](#ci---run-tests-with-github-actions) - [Tasks List](#tasks-list) @@ -193,7 +194,29 @@ } } ``` -9. [Nodemon](https://nodemon.io/) to have hot-reload of the running Express server code. +9. To make sure that all is working fine we'll create the new app (e.g. in the `src/index.ts`) +```typescript +import express, { Application, Request, Response } from 'express'; +import http from 'http'; + +const app: Application = express(); +const server = http.createServer(app); + +app.get('/', (req: Request, res: Response) => { + res.json({ greeting: 'Hello World!' }); +}); + +const PORT = 5000; + +server.listen(PORT); + +server.on('listening', () => { + console.info('server up listening'); +}); + +``` +This will create an Express application that will be listening on port `5000` and return the JSON (with dummy data greeting with `"Hello World!"`). +10. [Nodemon](https://nodemon.io/) to have hot-reload of the running Express server code. - install: ```bash npm i -D nodemon @@ -240,7 +263,7 @@ As we have TypeScript configured, then we don't need any additional setup. We're ### Unit tests with Jest -1. Install [Jest]() together with [ts-jest]() package and needed typings to make it work with TypeScript. +1. Install [Jest](https://jestjs.io/) together with [ts-jest](https://kulshekhar.github.io/ts-jest/) package and needed typings to make it work with TypeScript. ```bash npm i -D jest @types/jest ts-jest ``` @@ -322,6 +345,78 @@ Jest will be smart enough to find by convention all files with `.unit.test.ts` s } ``` +### API tests with SuperTest + +[SuperTest](https://github.com/visionmedia/supertest#readme) is a useful library that allows testing Express HTTP applications. + +To install it run: +```bash +npm i -D supertest @types/supertest +``` + +`SuperTest` takes as input Express application. We have to structure our code to return it, e.g. + +```typescript +import express, { Application, Request, Response } from 'express'; +import { getGreeting } from './greetings/getGreeting'; + +const app: Application = express(); + +app.get('/', (_req: Request, res: Response) => { + res.json(getGreeting()); +}); + +export default app; +``` + +Our updated intex will look like: + +```typescript +import app from './app'; +import http from 'http'; + +const server = http.createServer(app); + +const PORT = 5000; + +server.listen(PORT); + +server.on('listening', () => { + console.info('server up listening'); +}); + +``` + +Let's create the test for the default route. For that, create a file, e.g. `getGreetings.api.test.ts`. We'll be using a different prefix, `api.test.ts`, as those tests are not unit but integration/acceptance. They will be running the Express server. Having the Express app extracted, we can use the `SuperTest` library as: + +```typescript +import request from 'supertest'; +import app from '../app'; + +describe('GET /', () => { + it('should return greeting "Hello World!"', () => { + return request(app) + .get('/') + .expect('Content-Type', /json/) + .expect(200, { greeting: 'Hello World!' }); + }); +}); +``` + +`SuperTest` wraps the Express app and making the API calls easier. It also provides a set of useful methods to check the response params. + +As the final step we'll add a separate NPM script to [package.json](./samples/simple/package.json) for running API tests and also script to run all of them. + +```json +{ + "scripts": { + "test": "npm run test:unit && npm run test:api", // <-- added + "test:unit": "jest unit", + "test:api": "jest api" // <-- added + } +} +``` + ### CI - Run tests with Github Actions It's important to have your changes be verified during the pull request process. We'll use GitHub Actions as a sample of how to do that. You need to create the [.github/workflows](./.github/workflows) folder and putt there new file (e.g. [samples_simple.yml](./.github/workflows/samples_simple.yml)). This file will contain YAML configuration for your action: @@ -362,7 +457,7 @@ jobs: # run build - run: npm run build:ts # run tests - - run: npm run test:unit + - run: npm test ``` If you want to make sure that your code will be running properly for a few Node.js versions and different operating systems (e.g. because developers may have different environment configuration) then you can use matrix tests: @@ -409,7 +504,7 @@ jobs: # run build - run: npm run build:ts # run tests - - run: npm run test:unit + - run: npm test ``` ## Tasks List @@ -418,10 +513,12 @@ jobs: - [x] Initial ExpressJS boilerplate configuration [PR](https://github.com/oskardudycz/EventSourcing.JS/pull/1) - [x] Add VSCode debugging configuration [PR](https://github.com/oskardudycz/EventSourcing.JS/pull/2) - [x] Add Jest unit test configuration with VSCode debug settings [PR](https://github.com/oskardudycz/EventSourcing.JS/pull/3) - - [x] CI - Run tests with Github Actions - - [ ] Add Jest api tests with SuperTest + - [x] CI - Run tests with Github Actions [PR](https://github.com/oskardudycz/EventSourcing.JS/pull/4) + - [x] Add Jest API tests with SuperTest + - [ ] Configure Swagger - [ ] Start Live Coding on Twitch - [ ] Add EventStoreDB gRPC client samples with basic streams operations - [ ] Add samples for Aggregates - [ ] Add samples for Subscriptions and projections to SQL lite - [ ] Create project template like `Create React App` for creating `EventStoreDB Node.js App` +- [ ] Add React application diff --git a/samples/simple/package-lock.json b/samples/simple/package-lock.json index fcf859aa..f07796fd 100644 --- a/samples/simple/package-lock.json +++ b/samples/simple/package-lock.json @@ -1,6 +1,6 @@ { "name": "eventsourcing.js", - "version": "1.0.0-alpha.1", + "version": "4.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -915,6 +915,12 @@ "@types/node": "*" } }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, "@types/express": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", @@ -1051,6 +1057,25 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/superagent": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.10.tgz", + "integrity": "sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.10.tgz", + "integrity": "sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "@types/yargs": { "version": "15.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", @@ -2129,6 +2154,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -3225,6 +3256,12 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", + "dev": true + }, "fastq": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", @@ -3332,6 +3369,12 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -5976,6 +6019,17 @@ "read-pkg": "^2.0.0" } }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "readdirp": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", @@ -6524,6 +6578,17 @@ "dev": true, "optional": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -6881,6 +6946,23 @@ "define-properties": "^1.1.3" } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -6914,6 +6996,78 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "superagent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "qs": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.0.tgz", + "integrity": "sha512-yjACOWijC6L/kmPZZAsVBNY2zfHSIbpdpL977quseu56/8BZ2LoF5axK2bGhbzhVKt7V9xgWTtpyLbxwIoER0Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, + "supertest": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.1.3.tgz", + "integrity": "sha512-v2NVRyP73XDewKb65adz+yug1XMtmvij63qIWHZzSX8tp6wiq6xBLUy4SUAd2NII6wIipOmHT/FD9eicpJwdgQ==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^6.1.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -7451,6 +7605,12 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/samples/simple/package.json b/samples/simple/package.json index 3ecfa3ac..9b7e9e63 100644 --- a/samples/simple/package.json +++ b/samples/simple/package.json @@ -1,12 +1,14 @@ { "name": "eventsourcing.js", - "version": "4.0.0", + "version": "5.0.0", "description": "Samples of Event Sourcing in JavaScript and TypeScript", "main": "index.js", "scripts": { "build:ts": "tsc", "dev:start": "nodemon src/index.ts", + "test": "npm run test:unit && npm run test:api", "test:unit": "jest unit", + "test:api": "jest api", "lint": "npm run lint:eslint && npm run lint:prettier", "lint:prettier": "prettier --check \"**/**/!(*.d).{ts,json,md}\"", "lint:eslint": "eslint **/*.ts", @@ -39,6 +41,7 @@ "@types/express": "^4.17.11", "@types/jest": "^26.0.21", "@types/node": "^14.14.33", + "@types/supertest": "^2.0.10", "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", "eslint": "^7.21.0", @@ -52,6 +55,7 @@ "jest": "^26.6.3", "nodemon": "^2.0.7", "prettier": "^2.2.1", + "supertest": "^6.1.3", "ts-jest": "^26.5.4", "ts-node": "^9.1.1", "typescript": "^4.2.3" diff --git a/samples/simple/src/app.ts b/samples/simple/src/app.ts new file mode 100644 index 00000000..7d4f332b --- /dev/null +++ b/samples/simple/src/app.ts @@ -0,0 +1,10 @@ +import express, { Application, Request, Response } from 'express'; +import { getGreeting } from './greetings/getGreeting'; + +const app: Application = express(); + +app.get('/', (_req: Request, res: Response) => { + res.json(getGreeting()); +}); + +export default app; diff --git a/samples/simple/src/greetings/getGreetings.api.test.ts b/samples/simple/src/greetings/getGreetings.api.test.ts new file mode 100644 index 00000000..c917f763 --- /dev/null +++ b/samples/simple/src/greetings/getGreetings.api.test.ts @@ -0,0 +1,11 @@ +import request from 'supertest'; +import app from '../app'; + +describe('GET /', () => { + it('should return greeting "Hello World!"', () => { + return request(app) + .get('/') + .expect('Content-Type', /json/) + .expect(200, { greeting: 'Hello World!' }); + }); +}); diff --git a/samples/simple/src/index.ts b/samples/simple/src/index.ts index 949e6bd4..4fc1aece 100644 --- a/samples/simple/src/index.ts +++ b/samples/simple/src/index.ts @@ -1,14 +1,8 @@ -import express, { Application, Request, Response } from 'express'; -import { getGreeting } from './greetings/getGreeting'; +import app from './app'; import http from 'http'; -const app: Application = express(); const server = http.createServer(app); -app.get('/', (req: Request, res: Response) => { - res.json(getGreeting()); -}); - const PORT = 5000; server.listen(PORT);