├── .eslintrc ├── .gitignore ├── README.md ├── bin └── vanilla.js ├── example ├── utils.js └── utils.spec.js ├── package.json └── src ├── reporter.js ├── runner.js ├── suite.js └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "mocha": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "no-console": "off", 9 | "no-plusplus": "off", 10 | "no-restricted-syntax": "off", 11 | "no-underscore-dangle": "off", 12 | "no-unused-expressions": "off", 13 | "guard-for-in": "off", 14 | "prefer-const": "off", 15 | "no-await-in-loop": "off" 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > "What I cannot create, I do not understand." 2 | > 3 | > -- Richard P. Feynman 4 | 5 | # Vanilla - tiny test runner 6 | 7 | This is a very simple implementation of a test runner, which helps to understand how it works under the hood. As you might guess, a lot of the features are missing and you can add most of them yourself. 8 | 9 | __Here are some of the links we've seen at the talk:__ 10 | * [Install bin files in PATH using npm](https://docs.npmjs.com/files/package.json#bin) 11 | * [Using npm link from a directory to work with the bin file globally](https://docs.npmjs.com/cli/link#description) 12 | * [Shebang interpreter directive](https://en.wikipedia.org/wiki/Shebang_(Unix)) 13 | * [Env shell command](https://en.wikipedia.org/wiki/Env) 14 | * [Chai assertion library](https://github.com/chaijs/chai) 15 | * [Mocha test runner](https://github.com/mochajs/mocha) 16 | * [Test Anything Protocol](https://testanything.org/) 17 | * [Awesome list of Test Anything Protocol resources](https://github.com/sindresorhus/awesome-tap) 18 | * [Node's EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) 19 | * [Chalk - terminal string styling](https://github.com/chalk/chalk) 20 | * [Symbols](https://www.copypastecharacter.com/symbols) 21 | * [Emojis](http://getemoji.com/) 22 | 23 | ## Development 24 | Run the following commands from the terminal 25 | 26 | ```bash 27 | npm install 28 | npm test 29 | ``` 30 | 31 | ## Aditional features 32 | Try to implement the following features under the current infrastructure. 33 | 34 | 1. `before`/`after` 35 | 2. `beforeEach`/`afterEach` 36 | 3. `it.only` 37 | 4. `it.skip` 38 | 39 | __Guidance__ 40 | 41 | 1. Write the usage of the feature on the example project. 42 | 2. Run `npm test` and let it fail. 43 | 3. Add the relevant global to the `rewireGlobals` function. 44 | 4. Add the new state under the constructor. 45 | 5. Change the `run` function accordingly. 46 | 47 | __Good Luck!__ :smiley: 48 | 49 | ## FAQ 50 | * How can i install the global `vanilla` command? - Use `npm link` from the main directory. 51 | * How can i remove the global `vanilla` command? - You can use `npm unlink` from the vanilla directory. 52 | -------------------------------------------------------------------------------- /bin/vanilla.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const Runner = require('../src/runner.js'); 4 | const reporter = require('../src/reporter.js'); 5 | 6 | const runner = new Runner({ reporter }); 7 | 8 | runner.addFile('../example/utils.spec.js'); 9 | 10 | runner.run(); 11 | -------------------------------------------------------------------------------- /example/utils.js: -------------------------------------------------------------------------------- 1 | module.exports.sum = (...args) => 2 | args.reduce((sum, num) => sum + num, 0); 3 | 4 | module.exports.product = (...args) => 5 | args.reduce((product, num) => product * num, 1); 6 | -------------------------------------------------------------------------------- /example/utils.spec.js: -------------------------------------------------------------------------------- 1 | const { sum, product } = require('./utils'); 2 | const { expect } = require('chai'); 3 | 4 | describe('utils', () => { 5 | describe('sum', () => { 6 | it('should return the sum of two numbers', () => { 7 | expect(sum(1, 2)).to.equal(3); 8 | }); 9 | 10 | it('should return the sum of four numbers', () => { 11 | expect(sum(1, 2, 3, 4)).to.equal(10); 12 | }); 13 | }); 14 | 15 | describe('product', () => { 16 | it('should return the product of two numbers', () => { 17 | expect(product(1, 2)).to.equal(2); 18 | }); 19 | 20 | it('should return the product of four numbers', () => { 21 | expect(product(1, 2, 3, 4)).to.equal(24); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla", 3 | "version": "1.0.0", 4 | "description": "Tiny test runner for learning purposes", 5 | "main": "./src/runner.js", 6 | "bin": "./bin/vanilla.js", 7 | "scripts": { 8 | "test": "./bin/vanilla.js", 9 | "lint": "eslint ." 10 | }, 11 | "keywords": ["test-runner", "tiny", "vanilla", "testing", "runner", "simple"], 12 | "author": "Ran Yitzhaki", 13 | "license": "MIT", 14 | "dependencies": { 15 | "chalk": "~2.3.0", 16 | "tap-nyan": "~1.1.0", 17 | "tap-spec": "~4.1.1" 18 | }, 19 | "devDependencies": { 20 | "chai": "~4.1.2", 21 | "eslint": "~4.17.0", 22 | "eslint-config-airbnb-base": "~12.1.0", 23 | "eslint-plugin-import": "~2.8.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/reporter.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | module.exports = (runner) => { 4 | let depth = 0; 5 | let pass = 0; 6 | let fail = 0; 7 | 8 | const log = message => console.log(' '.repeat(depth) + message); 9 | 10 | runner.on('start', () => { 11 | console.log(chalk.green('start testing!')); 12 | console.log(''); 13 | }); 14 | 15 | runner.on('start-suite', ({ title }) => { 16 | depth++; 17 | log(title); 18 | }); 19 | 20 | runner.on('end-suite', () => { 21 | depth--; 22 | }); 23 | 24 | runner.on('test-pass', ({ title }) => { 25 | pass++; 26 | log(chalk.green(' ✓ ') + chalk.grey(title)); 27 | }); 28 | 29 | runner.on('test-fail', ({ title, error }) => { 30 | fail++; 31 | log(chalk.red(` ${fail}) ${title}`)); 32 | log(''); 33 | log(chalk.yellow(error.toString())); 34 | log(''); 35 | }); 36 | 37 | runner.on('end', () => { 38 | if (fail === 0) { 39 | console.log(''); 40 | console.log(chalk.green(` ${pass} tests pass`)); 41 | process.exit(0); 42 | } else { 43 | console.log(''); 44 | console.log(chalk.red(' tests failed 😥')); 45 | console.log(''); 46 | 47 | console.log(chalk.green(` pass: ${pass}`)); 48 | console.log(chalk.red(` fail: ${fail}`)); 49 | process.exit(1); 50 | } 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/runner.js: -------------------------------------------------------------------------------- 1 | const Suite = require('./suite'); 2 | const EventEmitter = require('events'); 3 | 4 | function rewireGlobals(context) { 5 | global.describe = context.addSuite.bind(context); 6 | global.it = context.addTest.bind(context); 7 | } 8 | 9 | function populateSuites(suites) { 10 | const suitesQueue = suites.slice(0); 11 | 12 | for (const suite of suitesQueue) { 13 | rewireGlobals(suite); 14 | suite.callback(); 15 | suite.suites.forEach(childSuite => suitesQueue.push(childSuite)); 16 | } 17 | } 18 | 19 | module.exports = class Runner extends EventEmitter { 20 | constructor({ reporter }) { 21 | super(); 22 | this.files = []; 23 | this.rootSuite = new Suite({ runner: this }); 24 | this.tests = []; 25 | this.reporter = reporter; 26 | } 27 | 28 | addFile(filePath) { 29 | this.files.push(filePath); 30 | } 31 | 32 | run() { 33 | this.reporter(this); 34 | 35 | rewireGlobals(this.rootSuite); 36 | this.files.forEach(require); 37 | populateSuites(this.rootSuite.suites); 38 | 39 | this.emit('start'); 40 | this.rootSuite.run(); 41 | this.emit('end'); 42 | } 43 | }; 44 | 45 | -------------------------------------------------------------------------------- /src/suite.js: -------------------------------------------------------------------------------- 1 | const Test = require('./test'); 2 | 3 | module.exports = class Suite { 4 | constructor({ title, callback, runner }) { 5 | this.title = title; 6 | this.runner = runner; 7 | this.callback = callback; 8 | this.suites = []; 9 | this.tests = []; 10 | } 11 | 12 | addSuite(title, callback) { 13 | this.suites.push(new Suite({ title, callback, runner: this.runner })); 14 | } 15 | 16 | addTest(title, callback) { 17 | this.tests.push(new Test({ title, callback, runner: this.runner })); 18 | } 19 | 20 | run() { 21 | this.tests.forEach(test => test.run()); 22 | this.suites.forEach((suite) => { 23 | this.runner.emit('start-suite', (suite)); 24 | suite.run(); 25 | this.runner.emit('end-suite'); 26 | }); 27 | } 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | module.exports = class Test { 2 | constructor({ title, callback, runner }) { 3 | this.title = title; 4 | this.callback = callback; 5 | this.runner = runner; 6 | } 7 | 8 | run() { 9 | try { 10 | this.callback(); 11 | this.runner.emit('test-pass', { title: this.title }); 12 | } catch (error) { 13 | this.runner.emit('test-fail', { title: this.title, error }); 14 | } 15 | } 16 | }; 17 | 18 | --------------------------------------------------------------------------------