├── .npmignore ├── .gitignore ├── .eslintignore ├── .babelrc ├── .travis.yml ├── test ├── .eslintrc ├── scenarios │ ├── no_live.js │ ├── localhost.js │ └── live_requests.js ├── utils │ ├── server.js │ └── runner.js └── test.js ├── .eslintrc ├── README.md ├── LICENSE ├── package.json └── src └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "0.10" 6 | script: 7 | - npm run test:ci 8 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "prefer-arrow-callback": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "rules": { 4 | "no-console": 0, 5 | "func-names": 0, 6 | "space-before-function-paren": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/scenarios/no_live.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | 3 | describe('no live requests', function() { 4 | it('passes', function() { 5 | assert(true); 6 | }); 7 | 8 | it('passes (async)', function(done) { 9 | setTimeout(done, 1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/utils/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const server = http.createServer(function(req, res) { 4 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 5 | res.end('Hello, world!\n'); 6 | }); 7 | 8 | exports.listen = function(port) { 9 | server.listen(port); 10 | }; 11 | 12 | exports.close = function(done) { 13 | server.close(done); 14 | }; 15 | -------------------------------------------------------------------------------- /test/scenarios/localhost.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | const request = require('request-promise'); 3 | 4 | const server = require('../utils/server'); 5 | 6 | 7 | describe('live requests', function() { 8 | before(function() { 9 | server.listen(8585); 10 | }); 11 | 12 | it('passes', function() { 13 | assert(true); 14 | }); 15 | 16 | it('requests localhost', function(done) { 17 | return request('http://localhost:8585') 18 | .then(function() { 19 | assert(true); 20 | done(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/scenarios/live_requests.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | const request = require('request-promise'); 3 | 4 | describe('live requests', function() { 5 | it('passes', function() { 6 | assert(true); 7 | }); 8 | 9 | it('requests isup.me', function(done) { 10 | return request('http://www.isup.me') 11 | .then(function() { 12 | assert(true); 13 | done(); 14 | }); 15 | }); 16 | 17 | it('requests github status', function(done) { 18 | return request('https://status.github.com') 19 | .then(function() { 20 | assert(true); 21 | done(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mocha-http-detect [![Build Status](https://travis-ci.org/Chris911/mocha-http-detect.svg?branch=master)](https://travis-ci.org/Chris911/mocha-http-detect) [![npm version](https://badge.fury.io/js/mocha-http-detect.svg)](https://badge.fury.io/js/mocha-http-detect) 2 | 3 | Detect live HTTP requests in your test suite. 4 | 5 | ![screenshot](http://i.imgur.com/00QZZlh.png) 6 | 7 | ## Description 8 | 9 | `mocha-http-detect` allows developers to report live HTTP requests in test suites. As a project grows, tests and dependencies can become difficult to manage. As a result, some tests might unwillingly call external services and in turn slow down tests and/or making them less reliable. By reporting live requests, this module identifies tests that might require stubbing calls to external services. 10 | 11 | ## Usage 12 | 13 | Install via NPM as a dev dependency: 14 | 15 | `$ npm i --save-dev mocha-http-detect` 16 | 17 | Run tests using the `--require flag`: 18 | 19 | `$ mocha --require mocha-http-detect test.js` 20 | -------------------------------------------------------------------------------- /test/utils/runner.js: -------------------------------------------------------------------------------- 1 | const spawn = require('child_process').spawn; 2 | 3 | /* 4 | * test runner helper 5 | */ 6 | function runMocha(args, fn) { 7 | const child = spawn('./node_modules/.bin/mocha', args.split(' ')); 8 | 9 | const result = { 10 | out: '', 11 | err: '', 12 | code: null, 13 | }; 14 | 15 | child.stdout.on('data', function (data) { 16 | result.out += data; 17 | }); 18 | 19 | child.stderr.on('data', function (data) { 20 | result.out += data; 21 | }); 22 | 23 | child.on('close', function (code) { 24 | result.code = code; 25 | fn(result); 26 | }); 27 | } 28 | 29 | /* 30 | * mocha test helper 31 | * Source: https://github.com/rstacruz/mocha-clean/blob/master/test/support/mocha.js 32 | 33 | * mocha('-R tap example/failure.js'); 34 | * 35 | * it('works', function () { 36 | * res.code 37 | * res.out 38 | * res.err 39 | * }); 40 | */ 41 | 42 | function mocha(args) { 43 | before(function(next) { 44 | runMocha(args, function (result) { 45 | global.res = result; 46 | next(); 47 | }); 48 | }); 49 | } 50 | 51 | module.exports = mocha; 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christophe Naud-Dulude 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocha-http-detect", 3 | "version": "0.2.0", 4 | "description": "Live request detection for Mocha", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "compile": "babel -d dist src", 8 | "lint": "eslint .", 9 | "pretest": "npm run lint", 10 | "test": "mocha --compilers js:babel-register --recursive", 11 | "pretest:ci": "npm run compile", 12 | "test:ci": "npm test" 13 | }, 14 | "keywords": [ 15 | "mocha", 16 | "live", 17 | "detect" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Chris911/mocha-http-detect.git" 22 | }, 23 | "author": "Christophe Naud-Dulude", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/Chris911/mocha-http-detect/issues" 27 | }, 28 | "homepage": "https://github.com/Chris911/mocha-http-detect", 29 | "dependencies": { 30 | "chalk": "1.1.1" 31 | }, 32 | "devDependencies": { 33 | "babel-cli": "^6.6.5", 34 | "babel-preset-es2015": "^6.6.0", 35 | "babel-register": "^6.7.2", 36 | "chai": "^3.5.0", 37 | "eslint": "2.3.0", 38 | "eslint-config-airbnb": "6.1.0", 39 | "mocha": "^2.4.5", 40 | "request-promise": "^2.0.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* globals res:false */ 2 | import { assert } from 'chai'; 3 | import mocha from './utils/runner.js'; 4 | 5 | 6 | describe('Mocha live detect', function() { 7 | describe('without live calls', function() { 8 | mocha('./test/scenarios/no_live.js -R tap --require dist/index.js'); 9 | 10 | it('does not log any live request', function() { 11 | assert.include(res.out, 'Hostnames requested'); 12 | assert.include(res.out, ' none'); 13 | assert.notInclude(res.out, 'Live requests'); 14 | }); 15 | }); 16 | 17 | describe('with local calls', function() { 18 | mocha('./test/scenarios/localhost.js -R tap --require dist/index.js'); 19 | 20 | it('does not log any live request', function() { 21 | assert.include(res.out, 'Hostnames requested'); 22 | assert.include(res.out, ' none'); 23 | assert.notInclude(res.out, 'Live requests'); 24 | }); 25 | }); 26 | 27 | describe('with live calls', function() { 28 | mocha('./test/scenarios/live_requests.js -R tap --require dist/index.js'); 29 | 30 | it('logs live call in test', function() { 31 | assert.include(res.out, 'Live requests'); 32 | assert.include(res.out, '* http://www.isup.me/'); 33 | assert.include(res.out, '* https://status.github.com/'); 34 | }); 35 | 36 | it('logs all hostnames', function() { 37 | assert.include(res.out, 'Hostnames requested:'); 38 | assert.include(res.out, 'www.isup.me: 1'); 39 | assert.include(res.out, 'tatus.github.com: 1'); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { EventEmitter } from 'events'; 3 | import http from 'http'; 4 | import { inherits } from 'util'; 5 | import Mocha from 'mocha'; 6 | 7 | const hostnames = {}; 8 | let testRequests = []; 9 | 10 | /** 11 | * Returns indent for pretty printing. 12 | * @param {integer} size - indent size in spaces 13 | * @return {string} the indent string 14 | */ 15 | function indent(size) { 16 | let indentStr = ''; 17 | let _size = size; 18 | while (_size > 0) { 19 | indentStr += ' '; 20 | _size--; 21 | } 22 | return indentStr; 23 | } 24 | 25 | /** 26 | * Prints full requests URL seen in test. 27 | */ 28 | function printTestRequest() { 29 | console.log(chalk.yellow(`${indent(6)} Live requests: `)); 30 | 31 | testRequests.forEach(request => { 32 | console.log(chalk.yellow(`${indent(8)} * ${request}`)); 33 | }); 34 | } 35 | 36 | /** 37 | * Print requested hostnames summary. 38 | */ 39 | function printHostnames() { 40 | console.log(chalk.yellow.bold(`${indent(2)} Hostnames requested: `)); 41 | 42 | if (Object.keys(hostnames).length === 0) { 43 | console.log(chalk.yellow(`${indent(4)}none`)); 44 | return; 45 | } 46 | 47 | for (const key in hostnames) { 48 | if (!hostnames[key]) return; 49 | console.log(chalk.yellow(`${indent(4)}${key}: ${hostnames[key]}`)); 50 | } 51 | } 52 | 53 | /** 54 | * Patch mocha to display recording requests during individual tests and at 55 | * the end of the test suite. 56 | */ 57 | function patchMocha() { 58 | const _testRun = Mocha.Test.prototype.run; 59 | 60 | Mocha.Test.prototype.run = function (fn) { 61 | function done(ctx) { 62 | fn.call(this, ctx); 63 | 64 | if (testRequests.length) { 65 | printTestRequest(testRequests); 66 | testRequests = []; 67 | } 68 | } 69 | 70 | return _testRun.call(this, done); 71 | }; 72 | 73 | const _run = Mocha.prototype.run; 74 | 75 | Mocha.prototype.run = function(fn) { 76 | function done(failures) { 77 | printHostnames(hostnames); 78 | fn.call(this, failures); 79 | } 80 | 81 | return _run.call(this, done); 82 | }; 83 | } 84 | 85 | /** 86 | * Get the hostname from an HTTP options object. 87 | * Supports multiple types of options. 88 | * @param {object} httpOptions 89 | * @return {string} the hostname or "Unknown" if not found. 90 | */ 91 | function getHostname(httpOptions) { 92 | if (httpOptions.uri && httpOptions.uri.hostname) { 93 | return httpOptions.uri.hostname; 94 | } else if (httpOptions.hostname) { 95 | return httpOptions.hostname; 96 | } else if (httpOptions.host) { 97 | return httpOptions.host; 98 | } 99 | return 'Unknown'; 100 | } 101 | 102 | /** 103 | * Get the href from an HTTP options objet. 104 | * Supports multiple types of options. 105 | * @param {object} httpOptions 106 | * @return {string} the hostname or "Unknown" if not found. 107 | */ 108 | function getHref(httpOptions) { 109 | if (httpOptions.uri && httpOptions.uri.href) { 110 | return httpOptions.uri.href; 111 | } else if (httpOptions.hostname && httpOptions.path) { 112 | return httpOptions.hostname + httpOptions.path; 113 | } else if (httpOptions.host && httpOptions.path) { 114 | return httpOptions.host + httpOptions.path; 115 | } 116 | return 'Unknown'; 117 | } 118 | 119 | /** 120 | * Patch Node's HTTP client to record external HTTP calls. 121 | * - All hostnames are stored in `hostname` with their count for the whole 122 | * test suite. 123 | * - Full request URLs are stores in `testRequests` and used to display 124 | * recorded requests for individual tests. 125 | * - Requests to localhost are ignored. 126 | */ 127 | function patchHttpClient() { 128 | const _ClientRequest = http.ClientRequest; 129 | 130 | function patchedHttpClient(options, done) { 131 | if (http.OutgoingMessage) http.OutgoingMessage.call(this); 132 | 133 | const hostname = getHostname(options); 134 | 135 | // Ignore localhost requests 136 | if (hostname.indexOf('127.0.0.1') === -1 && hostname.indexOf('localhost') === -1) { 137 | if (hostnames[hostname]) { 138 | hostnames[hostname]++; 139 | } else { 140 | hostnames[hostname] = 1; 141 | } 142 | 143 | testRequests.push(getHref(options)); 144 | } 145 | 146 | _ClientRequest.call(this, options, done); 147 | } 148 | 149 | if (http.ClientRequest) { 150 | inherits(patchedHttpClient, _ClientRequest); 151 | } else { 152 | inherits(patchedHttpClient, EventEmitter); 153 | } 154 | 155 | http.ClientRequest = patchedHttpClient; 156 | 157 | http.request = function(options, done) { 158 | return new http.ClientRequest(options, done); 159 | }; 160 | } 161 | 162 | patchHttpClient(); 163 | patchMocha(); 164 | --------------------------------------------------------------------------------