├── .editorconfig ├── .github └── workflows │ ├── pr.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.ts ├── stages.ts └── types.ts ├── tests ├── config.yml ├── docker-compose.yml ├── jest.config.js └── specs │ ├── __snapshots__ │ └── simple.test.ts.snap │ └── simple.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | paths-ignore: 8 | - README.md 9 | 10 | workflow_dispatch: 11 | 12 | jobs: 13 | tests: 14 | name: Tests 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - uses: actions/setup-node@v2-beta 20 | with: 21 | node-version: '16' 22 | 23 | - name: Run npm install 24 | run: npm ci 25 | 26 | - name: Build 27 | run: npm run build 28 | 29 | - name: Run Tests 30 | run: npm run test 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish New Version 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | publish: 12 | name: Publish 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - uses: actions/setup-node@v2-beta 18 | with: 19 | node-version: '16' 20 | 21 | - name: Automated Version Bump 22 | uses: phips28/gh-action-bump-version@v9.0.1 23 | with: 24 | tag-prefix: 'v' 25 | commit-message: 'CI: bumps version to {{version}} [skip ci]' 26 | 27 | - name: Run npm install 28 | run: npm ci 29 | env: 30 | NODE_ENV: production 31 | 32 | - name: Publish 33 | run: npm publish 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-compose-jest-runner 2 | 3 | This package allows to run tests that use `docker-compose` and supports multi-stage setup. 4 | 5 | ## How it works? 6 | 7 | This runner creates docker-compose services in stopped state and then starts them accordingly to the stages. It will start the new stage only when all the services on current one will be running. When all the stages are done the actual Jest tests will be run. After the tests will be completed the services will be teared down. 8 | 9 | ## Setup 10 | 11 | 1. Run `npm install --save-dev docker-compose-jest-runner` 12 | 2. Add `dc-jest-runner.yml` to the root repo or use `DC_JEST_RUNNER_CONFIG` environment variable for path to the config file. 13 | 3. Add `runner`: `docker-compose-jest-runner` to `jest.config.js` file. 14 | 15 | That's all 16 | 17 | ## Configuration 18 | 19 | ```yaml 20 | files: string | string[] (optional, default 'docker-compose.yaml') # docker-compose yaml files 21 | skipPull: boolean (optional, default false) # skips pulling docker images 22 | skipBuild: boolean (optional, default false) # skips building docker images 23 | timeout: number (optional, default Infinity) # maximum time in ms to wait for service on each stage 24 | interval: number (optional, default 250) # interval to check service in ms 25 | stages: 26 | - name: string 27 | services: 28 | - name: string # should be exactly as in docker-compose files 29 | timeout: number (optional, defaults to stage's value) 30 | interval: number (optional, defaults to stage's value) 31 | logs: boolean (optional, default false) # if "true" prints the container logs after tests execution 32 | check: string or object # based on `wait-on` npm package 33 | protocol: tcp | http | https | http-get | https-get 34 | port: number (optional, default 80 for http and 443 for https) 35 | path: string 36 | ``` 37 | 38 | Look [here](https://github.com/jeffbski/wait-on#usage) for more details regarding service check definition. 39 | 40 | ## Example 41 | 42 | ```yaml 43 | files: 44 | - ./tests/docker-compose.yml 45 | timeout: 2000 46 | interval: 100 47 | stages: 48 | - name: Infra 49 | services: 50 | - name: mongo 51 | check: 'tcp:localhost:27017' 52 | - name: Service 53 | services: 54 | - name: api 55 | logs: true 56 | check: 57 | port: 3000 58 | protocol: http-get 59 | path: /posts 60 | 61 | ``` 62 | 63 | ## Contributing 64 | 65 | ### Requirements 66 | 67 | 1. [Docker](https://www.docker.com/) 68 | 69 | 2. [NodeJS](https://nodejs.org/en/) 70 | 71 | ### Getting started 72 | 73 | 1. Clone the repo: 74 | 75 | ```sh 76 | git clone git@github.com:AleF83/docker-compose-jest-runner.git 77 | ``` 78 | 79 | 2. Install `npm` packages: 80 | 81 | ```sh 82 | npm ci 83 | ``` 84 | 85 | 3. Build the project: 86 | 87 | ```sh 88 | npm run build 89 | ``` 90 | 91 | 4. Run tests: 92 | 93 | ```sh 94 | npm run test 95 | ``` 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-compose-jest-runner", 3 | "version": "0.1.0", 4 | "description": "Jest Runner for setting up docker-compose environment by stages", 5 | "main": "./build/index.js", 6 | "types": "./build/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./build/index.d.ts", 10 | "default": "./build/index.js" 11 | }, 12 | "./package.json": "./package.json" 13 | }, 14 | "scripts": { 15 | "build": "tsc", 16 | "test": "DC_JEST_RUNNER_CONFIG=tests/config.yml jest --config tests/jest.config.js" 17 | }, 18 | "keywords": [], 19 | "author": "Alex Kotler Fux ", 20 | "license": "ISC", 21 | "engines": { 22 | "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" 23 | }, 24 | "dependencies": { 25 | "docker-compose": "^0.23.14", 26 | "jest-runner": "^27.4.2", 27 | "js-yaml": "^4.1.0", 28 | "wait-on": "^6.0.0" 29 | }, 30 | "devDependencies": { 31 | "@jest/types": "^27.4.2", 32 | "@types/jest": "^27.0.3", 33 | "@types/js-yaml": "^4.0.5", 34 | "@types/node": "^16.11.11", 35 | "@types/wait-on": "^5.3.1", 36 | "jest": "^27.4.2", 37 | "ts-jest": "^27.0.7", 38 | "typescript": "^4.5.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import TestRunner, { 2 | OnTestFailure, 3 | OnTestStart, 4 | OnTestSuccess, 5 | Test, 6 | TestRunnerContext, 7 | TestRunnerOptions, 8 | TestWatcher, 9 | } from "jest-runner"; 10 | import { Config } from "@jest/types"; 11 | 12 | import {init, up, down} from './stages'; 13 | export default class DockerComposeJestRunner extends TestRunner { 14 | constructor(globalConfig: Config.GlobalConfig, context?: TestRunnerContext) { 15 | super(globalConfig, context); 16 | } 17 | 18 | async runTests( 19 | tests: Array, 20 | watcher: TestWatcher, 21 | onStart: OnTestStart | undefined, 22 | onResult: OnTestSuccess | undefined, 23 | onFailure: OnTestFailure | undefined, 24 | options: TestRunnerOptions 25 | ) { 26 | await init(); 27 | await up(); 28 | 29 | super.runTests(tests, watcher, onStart, onResult, onFailure, options); 30 | 31 | await down(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/stages.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "fs/promises"; 2 | import * as yaml from 'js-yaml'; 3 | import dc, { IDockerComposeOptions } from 'docker-compose'; 4 | import * as waitOn from 'wait-on'; 5 | 6 | import { Check, Config, Service } from "./types"; 7 | 8 | const configFilePath = process.env.DC_JEST_RUNNER_CONFIG || 'dc-jest-runner.yml'; 9 | 10 | let config: Config; 11 | let dcOptions: IDockerComposeOptions; 12 | 13 | export async function init() { 14 | const configStr = await readFile(configFilePath, { encoding: "utf8" }); 15 | config = yaml.load(configStr) as Config; 16 | 17 | dcOptions = { 18 | env: { 19 | COMPOSE_DOCKER_CLI_BUILD: '1', 20 | DOCKER_BUILDKIT: '1', 21 | PATH: process.env.PATH, 22 | NODE_AUTH_TOKEN: process.env.NODE_AUTH_TOKEN, 23 | }, 24 | config: config.files, 25 | log: true, 26 | }; 27 | return dcOptions; 28 | } 29 | 30 | export async function up() { 31 | if (!config.skipPull) { 32 | await dc.pullAll(dcOptions); 33 | } 34 | 35 | if (!config.skipBuild) { 36 | await dc.buildAll(dcOptions); 37 | } 38 | 39 | await dc.upAll({ ...dcOptions, commandOptions: ['--no-start'] }); 40 | 41 | for (const stage of config.stages) { 42 | console.log(`Starting stage: ${stage.name}`); 43 | await dc.restartMany(stage.services.map(s=> s.name), dcOptions); 44 | 45 | console.log(`Waiting for services to be ready: ${stage.services.map(s => s.name).join(',')}`); 46 | await waitOn({ 47 | resources: buildResources(stage.services), 48 | timeout: stage.timeout ?? config.timeout, 49 | interval: stage.interval ?? config.interval, 50 | simultaneous: 1, 51 | }); 52 | console.log('Stage completed.'); 53 | } 54 | } 55 | 56 | export async function down() { 57 | const services = config.stages.flatMap(s => s.services).filter(s => s.logs).map(s => s.name); 58 | if (services.length > 0) { 59 | await dc.logs(services, dcOptions); 60 | } 61 | await dc.down(dcOptions); 62 | 63 | console.log('Waiting for all services to tear down...'); 64 | await waitOn({ 65 | resources: buildResources(config.stages.flatMap(s => s.services)), 66 | timeout: config.timeout, 67 | interval: config.interval, 68 | simultaneous: 1, 69 | reverse: true, 70 | }); 71 | console.log('All the services are down.'); 72 | } 73 | 74 | function buildResources(services: Service[]) { 75 | return services.map(s => { 76 | if (typeof s.check === 'string') return s.check; 77 | const check = s.check as Check; 78 | const host = check.host ?? 'localhost'; 79 | const port = check.port ?? (check.protocol.startsWith('https') ? 443 : 80); 80 | const protocol = check.protocol === 'tcp' ? `${check.protocol}:` : `${check.protocol}://`; 81 | const path = check.path ?? ''; 82 | return `${protocol}${host}:${port}${path}`; 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { CheckPrimeOptions } from "crypto"; 2 | 3 | export type Check = { 4 | protocol?: 'tcp' | 'http' | 'http-get' | 'https' | 'https-get'; 5 | host?: string; 6 | port?: number; 7 | path?: string; 8 | } 9 | 10 | export type Service = { 11 | name: string; 12 | check: string | CheckPrimeOptions; 13 | logs?: boolean; 14 | } 15 | 16 | export type Stage = { 17 | name: string; 18 | services: Service[]; 19 | timeout?: number; 20 | interval?: number; 21 | } 22 | 23 | export type Config = { 24 | skipPull?: boolean; 25 | skipBuild?: boolean; 26 | files: string[], 27 | stages: Stage[]; 28 | timeout?: number; 29 | interval?: number; 30 | }; 31 | -------------------------------------------------------------------------------- /tests/config.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - ./tests/docker-compose.yml 3 | timeout: 2000 4 | interval: 100 5 | stages: 6 | - name: Infra 7 | services: 8 | - name: mongo 9 | check: 'tcp:localhost:27017' 10 | - name: Service 11 | services: 12 | - name: api 13 | logs: true 14 | check: 15 | port: 3000 16 | protocol: http-get 17 | path: /posts 18 | -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | ports: 7 | - 27017:27017 8 | 9 | api: 10 | image: vimagick/json-server 11 | command: -H 0.0.0.0 -p 3000 -w db.json 12 | ports: 13 | - "3000:3000" 14 | -------------------------------------------------------------------------------- /tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | rootDir: '../', 4 | runner: './build', 5 | testEnvironment: 'node', 6 | testMatch: ['/tests/specs/*.test.ts'], 7 | }; 8 | -------------------------------------------------------------------------------- /tests/specs/__snapshots__/simple.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Simple test should work 1`] = ` 4 | "mongo: running 5 | vimagick/json-server: running" 6 | `; 7 | -------------------------------------------------------------------------------- /tests/specs/simple.test.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | 3 | describe("Simple test", () => { 4 | test("should work", async () => { 5 | const result = execSync('docker ps --format "{{.Image}}: {{.State}}" | sort', { encoding: "utf8" }).trim(); 6 | expect(result).toMatchSnapshot(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "moduleResolution": "node", 5 | "outDir": "./build", 6 | "target": "ES2017", 7 | "types": ["node", "jest"] 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | --------------------------------------------------------------------------------