├── .gitignore ├── .eslintignore ├── .prettierrc.json ├── .husky ├── pre-push └── pre-commit ├── jest.config.json ├── tsconfig.lint.json ├── src ├── index.ts ├── common │ └── types.ts ├── wiremock.ts ├── wiremock.spec.ts ├── plugin.ts ├── launcher.ts ├── plugin.spec.ts └── launcher.spec.ts ├── tsconfig.json ├── .editorconfig ├── .eslintrc.json ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node" 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["."], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { WiremockLauncher } from './launcher'; 2 | 3 | export default class WiremockService {} 4 | export const launcher = WiremockLauncher; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib/", 5 | "declaration": true 6 | }, 7 | "include": ["src"], 8 | "exclude": ["**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Tab indentation 7 | [*] 8 | indent_style = tab 9 | trim_trailing_whitespace = true 10 | 11 | [{*.yml,*.yaml}] 12 | indent_style = space 13 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@typescript-eslint/recommended", "prettier", "prettier/prettier"], 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": "./tsconfig.lint.json" 6 | }, 7 | "plugins": ["@typescript-eslint"], 8 | "settings": { 9 | "import/parsers": { 10 | "@typescript-eslint/parser": [".ts"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | interface Capability { 2 | [key: string]: unknown; 3 | } 4 | 5 | export interface Options { 6 | port?: number; 7 | rootDir?: string; 8 | downloadUrl?: string; 9 | args?: Array; 10 | version?: string; 11 | skipWiremockInstall?: boolean; 12 | silent?: boolean; 13 | binPath?: string; 14 | } 15 | 16 | export interface WdioConfig { 17 | watch?: boolean; 18 | [key: string]: unknown; 19 | } 20 | 21 | export type Capabilities = Array | { [key: string]: Capability }; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Erwin Heitzman 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 | -------------------------------------------------------------------------------- /src/wiremock.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs'; 2 | import { get } from 'https'; 3 | import type { IncomingMessage } from 'http'; 4 | 5 | export class WireMock { 6 | static async download(from: string, to: string) { 7 | process.stdout.write(`Downloading WireMock from ${from}\n`); 8 | const res = await this.httpRequest(from); 9 | return this.writeData(res, to); 10 | } 11 | 12 | private static writeData(data: IncomingMessage, to: string): Promise { 13 | return new Promise((resolve, reject) => { 14 | data.pipe(createWriteStream(to)); 15 | data.on('end', () => resolve()); 16 | data.on('error', () => reject(new Error('Could not write to ' + to))); 17 | }); 18 | } 19 | 20 | private static httpRequest(url: string): Promise { 21 | return new Promise((resolve, reject) => { 22 | const req = get(url, (res) => { 23 | if (res.statusCode !== 200) { 24 | return reject(new Error('statusCode=' + res.statusCode)); 25 | } 26 | 27 | return resolve(res); 28 | }); 29 | 30 | req.on('error', (err) => { 31 | reject(err); 32 | }); 33 | 34 | req.end(); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/wiremock.spec.ts: -------------------------------------------------------------------------------- 1 | const createWriteStream = jest.fn(); 2 | const downloadUrl = 'download-url'; 3 | const targetPath = 'target-path'; 4 | const dummyData = 'dummy-data'; 5 | 6 | jest.mock('fs', () => ({ 7 | createWriteStream, 8 | })); 9 | 10 | import { WireMock } from './wiremock'; 11 | 12 | WireMock['httpRequest'] = jest.fn(); 13 | WireMock['writeData'] = jest.fn(); 14 | process.stdout.write = jest.fn(); 15 | 16 | beforeEach(() => { 17 | (WireMock['httpRequest'] as jest.Mock).mockResolvedValue(dummyData); 18 | }); 19 | 20 | afterEach(() => { 21 | jest.resetAllMocks(); 22 | jest.restoreAllMocks(); 23 | jest.clearAllMocks(); 24 | }); 25 | 26 | it('should log a download message', async () => { 27 | await WireMock.download(downloadUrl, targetPath); 28 | expect(process.stdout.write).toHaveBeenCalledWith(`Downloading WireMock from ${downloadUrl}\n`); 29 | }); 30 | 31 | it('should write data', async () => { 32 | await WireMock.download(downloadUrl, targetPath); 33 | expect(WireMock['httpRequest']).toHaveBeenCalledWith(downloadUrl); 34 | expect(WireMock['writeData']).toHaveBeenCalledWith(dummyData, targetPath); 35 | }); 36 | 37 | it('should reject and throw error', async () => { 38 | (WireMock['httpRequest'] as jest.Mock).mockRejectedValue('rejected!'); 39 | try { 40 | await WireMock.download(downloadUrl, targetPath); 41 | } catch (error) { 42 | expect(error).toEqual('rejected!'); 43 | } 44 | }); 45 | 46 | it('should resolve without a value', async () => { 47 | WireMock['httpRequest'] as jest.Mock; 48 | const res = await WireMock.download(downloadUrl, targetPath); 49 | expect(res).toEqual(undefined); 50 | }); 51 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs'; 2 | import { resolve } from 'path'; 3 | import type { Options, Capabilities, WdioConfig } from './common/types'; 4 | 5 | export abstract class Plugin { 6 | protected readonly args: Array; 7 | protected readonly port: number; 8 | protected readonly url: string; 9 | protected readonly skipWiremockInstall: boolean; 10 | protected readonly watchMode: boolean; 11 | protected readonly binPath: string; 12 | protected readonly silent: boolean; 13 | 14 | constructor(options: Options = {}, capabilities?: Capabilities, config?: WdioConfig) { 15 | const args = options.args || []; 16 | const version = options.version || '3.3.1'; 17 | const rootDir = options.rootDir || 'wiremock'; 18 | 19 | if (!existsSync(rootDir)) { 20 | mkdirSync(rootDir, { recursive: true }); 21 | } 22 | 23 | if (args.includes('-port')) { 24 | throw new Error('Cannot set port using args. Use options.port instead.'); 25 | } 26 | 27 | if (args.includes('-root-dir')) { 28 | throw new Error('Cannot set root-dir using args. Use options.rootDir instead.'); 29 | } 30 | 31 | this.silent = !!options.silent; 32 | this.watchMode = !!config?.watch; 33 | this.binPath = options.binPath ? resolve(options.binPath) : resolve(__dirname, `wiremock-${version}.jar`); 34 | this.port = options.port || 8080; 35 | this.skipWiremockInstall = !!options.skipWiremockInstall; 36 | this.args = ['-jar', this.binPath, '-port', this.port.toString(), '-root-dir', rootDir].concat(args); 37 | this.url = 38 | options.downloadUrl || 39 | `https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/${version}/wiremock-standalone-${version}.jar`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/launcher.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { ChildProcess, spawn } from 'child_process'; 3 | import { waitUntilUsed, waitUntilFree } from 'tcp-port-used'; 4 | import { WireMock } from './wiremock'; 5 | import { Plugin } from './plugin'; 6 | import type { Options, Capabilities, WdioConfig } from './common/types'; 7 | 8 | export class WiremockLauncher extends Plugin { 9 | protected process!: ChildProcess | null; 10 | 11 | constructor(options: Options = {}, capabilities?: Capabilities, config?: WdioConfig) { 12 | super(options, capabilities, config); 13 | } 14 | 15 | async onPrepare(): Promise { 16 | if (!existsSync(this.binPath) && !this.skipWiremockInstall) { 17 | try { 18 | await WireMock.download(this.url, this.binPath); 19 | } catch (error) { 20 | throw new Error(`Downloading WireMock failed: ${error}\n`); 21 | } 22 | } 23 | 24 | this.process = spawn('java', this.args, { detached: true }); 25 | 26 | if (!this.silent && this.process.stdout && this.process.stderr) { 27 | this.process.stdout.pipe(process.stdout); 28 | this.process.stderr.pipe(process.stderr); 29 | } 30 | 31 | this.process.on('exit', () => { 32 | process.stdout.write(`Wiremock exited\n\n`); 33 | }); 34 | 35 | if (this.watchMode) { 36 | process.on('SIGINT', () => this.stopProcess(this.port)); 37 | process.on('exit', () => this.stopProcess(this.port)); 38 | process.on('uncaughtException', () => this.stopProcess(this.port)); 39 | } 40 | 41 | await waitUntilUsed(this.port, 100, 10000); 42 | } 43 | 44 | async onComplete(): Promise { 45 | if (!this.watchMode) { 46 | await this.stopProcess(this.port); 47 | } 48 | } 49 | 50 | private async stopProcess(port: number) { 51 | if (!this.process?.killed) { 52 | this.process?.kill(); 53 | } 54 | 55 | await waitUntilFree(port, 100, 10000); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wdio-wiremock-service", 3 | "version": "7.0.2", 4 | "description": "A WebdriverIO service to start & stop WireMock Standalone", 5 | "author": "Erwin Heitzman ", 6 | "homepage": "https://github.com/erwinheitzman/wdio-wiremock-service#readme", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "types": "lib/index.d.ts", 10 | "files": [ 11 | "/lib" 12 | ], 13 | "scripts": { 14 | "prepare": "husky install", 15 | "prepublishOnly": "tsc", 16 | "test": "jest" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/erwinheitzman/wdio-wiremock-service.git" 21 | }, 22 | "keywords": [ 23 | "webdriver", 24 | "webdriverio", 25 | "wdio", 26 | "wdio-service", 27 | "wiremock", 28 | "standalone", 29 | "server", 30 | "mock", 31 | "proxy", 32 | "stub", 33 | "tests" 34 | ], 35 | "bugs": { 36 | "url": "https://github.com/erwinheitzman/wdio-wiremock-service/issues" 37 | }, 38 | "peerDependencies": { 39 | "webdriverio": ">=7.0.0" 40 | }, 41 | "dependencies": { 42 | "tcp-port-used": "^1.0.2" 43 | }, 44 | "devDependencies": { 45 | "@tsconfig/node18": "^18.2.2", 46 | "@types/jest": "^29.5.10", 47 | "@types/node": "^20.10.0", 48 | "@types/tcp-port-used": "^1.0.4", 49 | "@typescript-eslint/eslint-plugin": "^6.12.0", 50 | "@typescript-eslint/parser": "^6.12.0", 51 | "eslint": "^8.54.0", 52 | "eslint-config-prettier": "^9.0.0", 53 | "husky": "^8.0.3", 54 | "jest": "^29.7.0", 55 | "lint-staged": "^15.1.0", 56 | "prettier": "^3.1.0", 57 | "ts-jest": "^29.1.1", 58 | "typescript": "^5.3.2" 59 | }, 60 | "husky": { 61 | "hooks": { 62 | "pre-commit": "lint-staged", 63 | "pre-push": "npm t" 64 | } 65 | }, 66 | "lint-staged": { 67 | "**/*.{js,ts}": [ 68 | "eslint --fix" 69 | ], 70 | "**/*.{yml,json,md,ts,js}": [ 71 | "prettier --write" 72 | ] 73 | }, 74 | "engines": { 75 | "node": ">=18" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 7.0.2 2 | 3 | - Fix types 4 | 5 | ## 7.0.1 6 | 7 | - Fix download url and download error message 8 | 9 | ## 7.0.0 10 | 11 | - Updated dependencies 12 | - Updated documentation 13 | - Added a downloadUrl option 14 | - changed the default Wiremock version to 3.3.1 15 | - changed the repository to download Wiremock from to GitHub instead of maven 16 | 17 | ## 6.0.7 18 | 19 | - Updated dependencies 20 | - Removed obsolete dependencies 21 | - Updated configurations 22 | - Changed Husky setup 23 | - Cleaned up project 24 | 25 | ## 6.0.6 26 | 27 | - fixed killing of process 28 | 29 | ## 6.0.5 30 | 31 | - Removed stdio option 32 | - Added silent mode 33 | - Updated dependencies 34 | 35 | ## 6.0.4 36 | 37 | - Fixed a minor issue with some parameter types 38 | - Updated dependencies 39 | 40 | ## 6.0.3 41 | 42 | - Split logic 43 | - Added more tests 44 | - Updated dependencies 45 | 46 | ## 6.0.2 47 | 48 | - Fixed Unrecognized option error when assigning arguments 49 | 50 | ## 6.0.0 - 6.0.1 51 | 52 | - Updated the service to be optimised for WebdriverIO v6 53 | - Fixed non-graceful shutdown when in watch mode 54 | 55 | ## 5.0.0 - 5.0.2 56 | 57 | Had to re-release because of the missing `lib` directory on publish. Will investigate in order to get the release process setup automatically. 58 | 59 | ### Breaking changes 60 | 61 | - Changed the default rootDir value from `mock` to `wiremock`.\ 62 | Fix: simply rename the directory to `wiremock` or add the `rootDir` property to the options object and pass the value `mock` to keep the directory as it is. 63 | 64 | ### Notable changes 65 | 66 | - Rewrote the service in TypeScript 67 | - Added tests 68 | - Added linting 69 | - Added styling 70 | - Added type declarations to the output 71 | - Fixed arguments assignment issue 72 | - Added WebdriverIO 5.X.X as a peer dependency 73 | - Changed version to 5.0.0 as this service will add support for both WebdriverIO 5 and soon WebdriverIO 6 74 | 75 | ## 2.26.4 76 | 77 | - Fixed options assignments 78 | 79 | ## 2.26.3 80 | 81 | - Fixed port assignment issue 82 | - Fixed issue where the arguments weren't passed correctly 83 | - Updated default version to 2.26.3 84 | 85 | ## 2.26.2 86 | 87 | - Added support for passing a different version 88 | 89 | ## 2.26.1 90 | 91 | - Added examples using the HTTP API to the docs 92 | 93 | ## 2.26.0 94 | 95 | - Updated to WireMock 2.26.0 96 | 97 | ## 2.25.1-8 98 | 99 | - Added support for relative path for rootDir 100 | - Added auto creation of the rootDir when it does not exist 101 | - Fixed typo in README.md 102 | 103 | ## 2.25.1-7 104 | 105 | - Added a default object to the options parameter for configuring the service 106 | - Updated readme with examples, fixed typo 107 | 108 | ## 2.25.1-6 109 | 110 | - Fixed logging on exit using wdio as standalone 111 | 112 | ## 2.25.1-5 113 | 114 | - Fix port not being set correctly 115 | 116 | ## 2.25.1-4 117 | 118 | - Fixed bug where a reference was incorrect 119 | 120 | ## 2.25.1-3 121 | 122 | - Added args support 123 | - Bug fixes 124 | - Correctly added dependency `tcp-port-used` 125 | 126 | ## 2.25.1-2 127 | 128 | - Added options support 129 | - Updated readme 130 | - Cleaned up code 131 | 132 | ## 2.25.1-1 133 | 134 | - First working version of the service 135 | -------------------------------------------------------------------------------- /src/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | const resolve = jest.fn(); 2 | const existsSync = jest.fn(); 3 | const mkdirSync = jest.fn(); 4 | 5 | let instance; 6 | 7 | jest.mock('path', () => ({ 8 | resolve, 9 | })); 10 | 11 | jest.mock('fs', () => ({ 12 | existsSync, 13 | mkdirSync, 14 | })); 15 | 16 | import { WiremockLauncher } from './launcher'; 17 | import { WireMock } from './wiremock'; 18 | 19 | WireMock['download'] = jest.fn(); 20 | 21 | beforeEach(() => { 22 | instance = null; 23 | }); 24 | 25 | afterEach(() => { 26 | jest.resetAllMocks(); 27 | jest.restoreAllMocks(); 28 | jest.clearAllMocks(); 29 | }); 30 | 31 | it('should have default port', () => { 32 | instance = new WiremockLauncher(); 33 | expect(instance['port']).toEqual(8080); 34 | }); 35 | 36 | it('should assign custom port', () => { 37 | instance = new WiremockLauncher({ port: 9999 }); 38 | expect(instance['port']).toEqual(9999); 39 | }); 40 | 41 | it('should have default rootDir', () => { 42 | existsSync.mockReturnValue(false); 43 | instance = new WiremockLauncher(); 44 | expect(mkdirSync).toHaveBeenCalledWith('wiremock', { recursive: true }); 45 | }); 46 | 47 | it('should assign custom rootDir', () => { 48 | existsSync.mockReturnValue(false); 49 | instance = new WiremockLauncher({ rootDir: 'example' }); 50 | expect(mkdirSync).toHaveBeenCalledWith('example', { recursive: true }); 51 | }); 52 | 53 | it('should have silent mode set to false', () => { 54 | instance = new WiremockLauncher(); 55 | expect(instance['silent']).toEqual(false); 56 | }); 57 | 58 | it('should assign silent mode', () => { 59 | instance = new WiremockLauncher({ silent: true }); 60 | expect(instance['silent']).toEqual(true); 61 | }); 62 | 63 | it('should have default url', () => { 64 | instance = new WiremockLauncher(); 65 | expect(instance['url']).toEqual( 66 | 'https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.3.1/wiremock-standalone-3.3.1.jar', 67 | ); 68 | }); 69 | 70 | it('should assign custom url', () => { 71 | instance = new WiremockLauncher({ downloadUrl: 'custom-url' }); 72 | expect(instance['url']).toEqual('custom-url'); 73 | }); 74 | 75 | it('should assign version to url', () => { 76 | const customVersion = '9.9.999'; 77 | instance = new WiremockLauncher({ version: customVersion }); 78 | expect(instance['url']).toEqual( 79 | `https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/${customVersion}/wiremock-standalone-${customVersion}.jar`, 80 | ); 81 | }); 82 | 83 | it('should assign false to watchMode', () => { 84 | instance = new WiremockLauncher({}, [{ browserName: 'chrome' }], { watch: false }); 85 | expect(instance['watchMode']).toEqual(false); 86 | }); 87 | 88 | it('should assign true to watchMode', () => { 89 | instance = new WiremockLauncher({}, [{ browserName: 'chrome' }], { watch: true }); 90 | expect(instance['watchMode']).toEqual(true); 91 | }); 92 | 93 | it('should assign binary path to binPath', () => { 94 | resolve.mockReturnValue('dummy'); 95 | instance = new WiremockLauncher(); 96 | expect(resolve).toHaveBeenCalledWith(__dirname, `wiremock-3.3.1.jar`); 97 | expect(instance['binPath']).toEqual('dummy'); 98 | }); 99 | 100 | it('should assign false to skipWiremockInstall', () => { 101 | instance = new WiremockLauncher({ skipWiremockInstall: false }); 102 | expect(instance['skipWiremockInstall']).toEqual(false); 103 | }); 104 | 105 | it('should assign true to skipWiremockInstall', () => { 106 | instance = new WiremockLauncher({ skipWiremockInstall: true }); 107 | expect(instance['skipWiremockInstall']).toEqual(true); 108 | }); 109 | 110 | it('should have default args', () => { 111 | resolve.mockReturnValue('dummy'); 112 | instance = new WiremockLauncher(); 113 | expect(instance['args']).toEqual(['-jar', 'dummy', '-port', '8080', '-root-dir', 'wiremock']); 114 | }); 115 | 116 | it('should assign args', () => { 117 | resolve.mockReturnValue('dummy'); 118 | instance = new WiremockLauncher({ port: 9999, rootDir: 'dummy-dir', args: ['--test', 'test'] }); 119 | expect(instance['args']).toEqual(['-jar', 'dummy', '-port', '9999', '-root-dir', 'dummy-dir', '--test', 'test']); 120 | }); 121 | 122 | it('should throw error when trying to set port using the args', () => { 123 | expect(() => new WiremockLauncher({ args: ['-port', '9999'] })).toThrowError( 124 | 'Cannot set port using args. Use options.port instead.', 125 | ); 126 | }); 127 | 128 | it('should throw error when to set root-dir using the args', () => { 129 | expect(() => new WiremockLauncher({ args: ['-root-dir', 'dummy'] })).toThrowError( 130 | 'Cannot set root-dir using args. Use options.rootDir instead.', 131 | ); 132 | }); 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebdriverIO WireMock Service 2 | 3 | [![npm version](https://badge.fury.io/js/wdio-wiremock-service.svg)](https://www.npmjs.com/package/wdio-wiremock-service) 4 | [![downloads](https://img.shields.io/npm/dm/wdio-wiremock-service.svg)](https://www.npmjs.com/package/wdio-wiremock-service) 5 | 6 | [![Join the chat at https://gitter.im/erwinheitzman/wdio-wiremock-service](https://badges.gitter.im/erwinheitzman/wdio-wiremock-service.svg)](https://gitter.im/erwinheitzman/wdio-wiremock-service?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | This service helps you to run [WireMock](http://wiremock.org/) seamlessly when running tests with [WebdriverIO](https://webdriver.io). It uses the well known [Maven](https://mvnrepository.com/repos/central) repository to download the WireMock jar for you which is then automatically installed, started and stopped. Stay up to date by joining the community over at [Gitter](https://gitter.im/erwinheitzman/wdio-wiremock-service) for help and support. 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm i -D wdio-wiremock-service 14 | ``` 15 | 16 | Instructions on how to install `WebdriverIO` can be found [here.](https://webdriver.io/docs/gettingstarted.html) 17 | 18 | ## Usage 19 | 20 | In the root directory (default `./mock`) you find two subdirectories, `__files` and `mappings` which are used for your fixtures and mocks. 21 | 22 | For more information, checkout [WireMock's official documentation](https://wiremock.org/docs/standalone/). 23 | 24 | ## Configuration 25 | 26 | In order to use the service with the wdio testrunner you need to add it to your service array: 27 | 28 | ```js 29 | // wdio.conf.js 30 | export.config = { 31 | // ... 32 | services: ['wiremock'], 33 | // ... 34 | }; 35 | ``` 36 | 37 | When using webdriverio standalone you need to add the service and trigger the `onPrepare` and `onComplete` hooks manually. An example can be found [here](####webdriverio-standalone) (the example makes use of [Jest](https://jestjs.io/en/)): 38 | 39 | ## Options 40 | 41 | The following options can be added to the service. 42 | 43 | ### port 44 | 45 | Port where WireMock should run on. 46 | 47 | Type: `Number` 48 | 49 | Default: `8080` 50 | 51 | Example: 52 | 53 | ```js 54 | // wdio.conf.js 55 | export const config = { 56 | // ... 57 | services: [['wiremock', { port: 8181 }]], 58 | // ... 59 | }; 60 | ``` 61 | 62 | ### rootDir 63 | 64 | Path where WireMock will look for files. 65 | 66 | Type: `String` 67 | 68 | Default: `./mock` 69 | 70 | Example: 71 | 72 | ```js 73 | // wdio.conf.js 74 | export const config = { 75 | // ... 76 | services: [['wiremock', { rootDir: './mock' }]], 77 | // ... 78 | }; 79 | ``` 80 | 81 | ### version 82 | 83 | Version of WireMock to be downloaded and used. 84 | 85 | Type: `String` 86 | 87 | Default: `3.3.1` 88 | 89 | Example: 90 | 91 | ```js 92 | // wdio.conf.js 93 | export const config = { 94 | // ... 95 | services: [['wiremock', { version: '2.25.1' }]], 96 | // ... 97 | }; 98 | ``` 99 | 100 | ### skipWiremockInstall 101 | 102 | Tell the service to skip downloading WireMock. 103 | 104 | Type: `Boolean` 105 | 106 | Default: false 107 | 108 | Example: 109 | 110 | ```js 111 | // wdio.conf.js 112 | export const config = { 113 | // ... 114 | services: [['wiremock', { skipWiremockInstall: true }]], 115 | // ... 116 | }; 117 | ``` 118 | 119 | ### binPath 120 | 121 | Custom path to a local Wiremock binary (often used in combination with skipWiremockInstall). 122 | 123 | Type: `String` 124 | 125 | Default: './wiremock-standalone-3.0.0.jar' (relative from service) 126 | 127 | Example: 128 | 129 | ```js 130 | // wdio.conf.js 131 | export const config = { 132 | // ... 133 | services: [['wiremock', { binPath: './my-custom/example-path/wiremock-standalone-3.0.0.jar' }]], 134 | // ... 135 | }; 136 | ``` 137 | 138 | ### silent 139 | 140 | Silent mode for logging WireMock's output (including additional logging from the service itself). 141 | 142 | Type: `Boolean` 143 | 144 | Default: `false` 145 | 146 | Example: 147 | 148 | ```js 149 | // wdio.conf.js 150 | export const config = { 151 | // ... 152 | services: [['wiremock', { silent: true }]], 153 | // ... 154 | }; 155 | ``` 156 | 157 | ### mavenBaseUrl 158 | 159 | Base download url for Maven. 160 | 161 | Type: `String` 162 | 163 | Default: `https://repo1.maven.org/maven2` 164 | 165 | Example: 166 | 167 | ```js 168 | // wdio.conf.js 169 | export const config = { 170 | // ... 171 | services: [['wiremock', { mavenBaseUrl: 'https://repo1.maven.org/maven2' }]], 172 | // ... 173 | }; 174 | ``` 175 | 176 | ### args 177 | 178 | List where you can pass all the supported arguments for configuring WireMock 179 | 180 | Note: you cannot pass the options (`port`, `rootDir`, `stdio`, `mavenBaseUrl`) here as they will be ignored. 181 | 182 | Type: `Array` 183 | 184 | Example: 185 | 186 | ```js 187 | // wdio.conf.js 188 | export const config = { 189 | // ... 190 | services: [ 191 | [ 192 | 'wiremock', 193 | { 194 | args: ['--verbose', '--match-headers'], 195 | }, 196 | ], 197 | ], 198 | // ... 199 | }; 200 | ``` 201 | 202 | ### Writing tests 203 | 204 | Writing your first test is really straight forward: 205 | 206 | #### Using the WDIO testrunner 207 | 208 | ```js 209 | import fetch from 'node-fetch'; // you can use any HTTP client you like 210 | import { equal } from 'node:assert'; // you can use any assertion library you like 211 | 212 | describe('example', () => { 213 | it(`should assert the mock data`, async () => { 214 | const body = await fetch('http://localhost:8080/api/mytest'); 215 | equal(body.text(), 'Hello world!'); 216 | }); 217 | }); 218 | ``` 219 | 220 | #### Using WebdriverIO Standalone 221 | 222 | ```js 223 | import fetch from 'node-fetch'; // you can use any HTTP client you like 224 | import { equal } from 'node:assert'; // you can use any assertion library you like 225 | import { remote } from 'webdriverio'; 226 | import { launcher } from 'wdio-wiremock-service'; 227 | 228 | const WDIO_OPTIONS = { 229 | capabilities: { 230 | browserName: 'chrome', 231 | }, 232 | }; 233 | 234 | describe('example', () => { 235 | let wiremockLauncher; 236 | let client; 237 | 238 | beforeAll(async () => { 239 | wiremockLauncher = new launcher(); // create instance of the service 240 | await wiremockLauncher.onPrepare(WDIO_OPTIONS); // run the onPrepare hook 241 | client = await remote(WDIO_OPTIONS); 242 | }); 243 | 244 | afterAll(async () => { 245 | await client.deleteSession(); 246 | await wiremockLauncher.onComplete(); // run the onComplete hook 247 | }); 248 | 249 | test('should showoff a mocked api response', async () => { 250 | const body = await fetch('http://localhost:8080/api/mytest'); 251 | equal(body.text(), 'Hello world!'); 252 | }); 253 | }); 254 | ``` 255 | 256 | For more information on WebdriverIO see the [homepage](https://webdriver.io). 257 | -------------------------------------------------------------------------------- /src/launcher.spec.ts: -------------------------------------------------------------------------------- 1 | const waitUntilUsed = jest.fn(); 2 | const waitUntilFree = jest.fn(); 3 | const existsSync = jest.fn(); 4 | const mockStdoutPipe = jest.fn(); 5 | const mockStderrPipe = jest.fn(); 6 | const spawn = jest.fn(); 7 | 8 | let defaultArgs: Array; 9 | 10 | jest.mock('child_process', () => ({ 11 | spawn, 12 | })); 13 | 14 | jest.mock('tcp-port-used', () => ({ 15 | waitUntilUsed, 16 | waitUntilFree, 17 | })); 18 | 19 | jest.mock('fs', () => ({ 20 | existsSync, 21 | mkdirSync: jest.fn(), 22 | })); 23 | 24 | jest.mock('./wiremock'); 25 | 26 | import { resolve } from 'path'; 27 | import { WiremockLauncher } from './launcher'; 28 | import { WireMock } from './wiremock'; 29 | 30 | beforeEach(() => { 31 | defaultArgs = ['-jar', resolve(__dirname, 'wiremock-3.3.1.jar'), '-port', '8080', '-root-dir', 'wiremock']; 32 | spawn.mockReturnValue({ 33 | stdout: { pipe: mockStdoutPipe }, 34 | stderr: { pipe: mockStderrPipe }, 35 | on: jest.fn(), 36 | }); 37 | }); 38 | 39 | afterEach(() => { 40 | jest.resetAllMocks(); 41 | jest.restoreAllMocks(); 42 | jest.clearAllMocks(); 43 | }); 44 | 45 | it('should start the service when no options are passed', async () => { 46 | const instance = new WiremockLauncher(); 47 | 48 | await instance.onPrepare(); 49 | 50 | expect(spawn).toHaveBeenCalledTimes(1); 51 | expect(spawn).toHaveBeenCalledWith('java', defaultArgs, { detached: true }); 52 | }); 53 | 54 | it('should start the service when a empty options is passed', async () => { 55 | const instance = new WiremockLauncher({}); 56 | 57 | await instance.onPrepare(); 58 | 59 | expect(spawn).toHaveBeenCalledTimes(1); 60 | expect(spawn).toHaveBeenCalledWith('java', defaultArgs, { detached: true }); 61 | }); 62 | 63 | it('should run wiremock with custom port', async () => { 64 | const instance = new WiremockLauncher({ port: 9999 }); 65 | defaultArgs[3] = '9999'; 66 | 67 | await instance.onPrepare(); 68 | 69 | expect(spawn).toHaveBeenCalledTimes(1); 70 | expect(spawn).toHaveBeenCalledWith('java', defaultArgs, { detached: true }); 71 | }); 72 | 73 | it('should run wiremock with custom rootDir', async () => { 74 | const instance = new WiremockLauncher({ rootDir: 'example' }); 75 | defaultArgs[5] = 'example'; 76 | 77 | await instance.onPrepare(); 78 | 79 | expect(spawn).toHaveBeenCalledTimes(1); 80 | expect(spawn).toHaveBeenCalledWith('java', defaultArgs, { detached: true }); 81 | }); 82 | 83 | it('should run wiremock with silent mode', async () => { 84 | const instance = new WiremockLauncher({ silent: true }); 85 | 86 | await instance.onPrepare(); 87 | 88 | expect(instance['silent']).toEqual(true); 89 | expect(mockStdoutPipe).toHaveBeenCalledTimes(0); 90 | expect(mockStderrPipe).toHaveBeenCalledTimes(0); 91 | }); 92 | 93 | it('should run wiremock without silent mode', async () => { 94 | const instance = new WiremockLauncher({ silent: false }); 95 | 96 | await instance.onPrepare(); 97 | 98 | expect(instance['silent']).toEqual(false); 99 | expect(mockStdoutPipe).toHaveBeenCalledTimes(1); 100 | expect(mockStderrPipe).toHaveBeenCalledTimes(1); 101 | }); 102 | 103 | it('should run wiremock with custom mavenBaseUrl', async () => { 104 | const instance = new WiremockLauncher({ downloadUrl: 'example-url' }); 105 | existsSync.mockReturnValue(false); 106 | 107 | await instance.onPrepare(); 108 | 109 | expect(WireMock.download).toHaveBeenCalledWith('example-url', defaultArgs[1]); 110 | expect(spawn).toHaveBeenCalledTimes(1); 111 | expect(spawn).toHaveBeenCalledWith('java', defaultArgs, { detached: true }); 112 | }); 113 | 114 | it('should throw when WireMock.download rejects', async () => { 115 | const rejectedError = 'rejected!'; 116 | const errorMessage = new Error(`Downloading WireMock failed: ${rejectedError}\n`); 117 | const instance = new WiremockLauncher(); 118 | 119 | existsSync.mockReturnValue(false); 120 | (WireMock['download'] as jest.Mock).mockRejectedValue(rejectedError); 121 | 122 | await expect(instance.onPrepare()).rejects.toThrowError(errorMessage); 123 | }); 124 | 125 | it('should not install wiremock when skipWiremockInstall is set to true', async () => { 126 | const instance = new WiremockLauncher({ skipWiremockInstall: true }); 127 | 128 | await instance.onPrepare(); 129 | 130 | expect(WireMock.download).toHaveBeenCalledTimes(0); 131 | }); 132 | 133 | it('should install wiremock when skipWiremockInstall is set to false', async () => { 134 | const instance = new WiremockLauncher({ skipWiremockInstall: false }); 135 | existsSync.mockReturnValue(false); 136 | 137 | await instance.onPrepare(); 138 | 139 | expect(WireMock.download).toHaveBeenCalledTimes(1); 140 | }); 141 | 142 | it('should concatenate the passed arguments with the required arguments', async () => { 143 | const args = ['--disable-http', '--bind-address', '0.0.0.0']; 144 | const instance = new WiremockLauncher({ args }); 145 | 146 | await instance.onPrepare(); 147 | 148 | expect(spawn).toHaveBeenCalledTimes(1); 149 | expect(spawn).toHaveBeenCalledWith('java', defaultArgs.concat(args), { detached: true }); 150 | }); 151 | 152 | it('should assign the stopProcess method to process.on when watchMode is active', async () => { 153 | const mockCallback = jest.fn(); 154 | const mockProcessOn = jest.spyOn(process, 'on').mockImplementation(mockCallback); 155 | const instance = new WiremockLauncher({}, [{ browserName: 'chrome' }], { watch: true }); 156 | 157 | await instance.onPrepare(); 158 | 159 | expect(spawn).toHaveBeenCalledTimes(1); 160 | expect(mockProcessOn).toHaveBeenCalledTimes(3); 161 | expect(mockProcessOn.mock.calls[0][0]).toEqual('SIGINT'); 162 | expect(mockProcessOn.mock.calls[1][0]).toEqual('exit'); 163 | expect(mockProcessOn.mock.calls[2][0]).toEqual('uncaughtException'); 164 | expect(mockCallback).toBeCalledTimes(3); 165 | }); 166 | 167 | it('should not execute the stopProcess method on completion when watchMode is active', async () => { 168 | const instance = new WiremockLauncher({}, [{ browserName: 'chrome' }], { watch: true }); 169 | instance['stopProcess'] = jest.fn(); 170 | 171 | await instance.onComplete(); 172 | 173 | expect(instance['stopProcess']).toHaveBeenCalledTimes(0); 174 | }); 175 | 176 | it('should execute the stopProcess method on completion when watchMode is not active', async () => { 177 | const instance = new WiremockLauncher({}, [{ browserName: 'chrome' }], { watch: false }); 178 | instance['stopProcess'] = jest.fn(); 179 | 180 | await instance.onComplete(); 181 | 182 | expect(instance['stopProcess']).toHaveBeenCalledTimes(1); 183 | }); 184 | 185 | it('should throw when waitUntilUsed rejects', async () => { 186 | const instance = new WiremockLauncher(); 187 | waitUntilUsed.mockRejectedValue(new Error('Error: timeout')); 188 | 189 | await expect(instance.onPrepare()).rejects.toThrowError('Error: timeout'); 190 | }); 191 | 192 | it('should throw when waitUntilFree rejects', async () => { 193 | const instance = new WiremockLauncher(); 194 | waitUntilFree.mockRejectedValue(new Error('Error: timeout')); 195 | 196 | await expect(instance.onComplete()).rejects.toThrowError('Error: timeout'); 197 | }); 198 | --------------------------------------------------------------------------------