├── .gitignore ├── README.md ├── karma.conf.js ├── package.json ├── src ├── client.js └── function-to-test.js ├── static ├── .gitkeep └── index.html ├── test ├── integration │ ├── .gitkeep │ └── sample-integration.spec.js ├── out │ └── screens │ │ └── .gitkeep └── unit │ ├── .gitkeep │ └── sample-unit.spec.js ├── wdio.conf.js └── webpack.conf.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /test/out/screens/* 3 | /static/client.js 4 | /static/client.js.map 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Front-end Javascript Testing 2 | 3 | Goals: 4 | 5 | 1. Unit testing 6 | 2. Integrations testing 7 | 8 | The libraries used for both of them have been 9 | kept as similar as possible to each other. 10 | In fact, only the runner library differs, 11 | but the framework and assertions libraries used are the same. 12 | 13 | This module aims to be a reference/ starter 14 | for a testable front-end Javascript application. 15 | 16 | ## Unit testing 17 | 18 | Unit tests will need to test individual functions (white-box) using: 19 | 20 | - Runner: Karma 21 | - Framework: Mocha 22 | - Assertions: Chai Expect, Chai-as-promised 23 | 24 | Individual functions will be required, 25 | and tested in isolation. 26 | This is white box testing, 27 | as we are concerned with the internal details of how each function works. 28 | Karma is used as the test runner 29 | rather than Mocha directly, 30 | because we need access browser functionality. 31 | 32 | ## Integration testing 33 | 34 | Integration tests will need to test entire application (black-box) using: 35 | 36 | - Runner: Webdriver.io 37 | - Framework: Mocha 38 | - Assertions: Chai Expect, Chai-as-promised 39 | 40 | The entire application will be run, 41 | and tests will simulation actual usage of the web site. 42 | This is black box testing, 43 | as we are not concerned with the details of how the application works, 44 | just the end results. 45 | Webdriver.io is used as the test runner, 46 | rather than Mocha directly, 47 | because it is used to interface with a selenium server. 48 | 49 | ## Author 50 | 51 | [Brendan Graetz](http://bguiz.com) 52 | 53 | ## Licence 54 | 55 | GPL-3.0 56 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = karmaConfig; 4 | 5 | function karmaConfig(configuration) { 6 | configuration.set({ 7 | autoWatch: true, 8 | basePath: '', 9 | browsers: ['Chrome'], 10 | colors: true, 11 | preprocessors: { 12 | 'test/unit/**/*.js': ['webpack'], 13 | }, 14 | files: [ 15 | 'test/unit/**/*.spec.js' 16 | ], 17 | frameworks: [ 18 | 'mocha', 19 | 'sinon-chai' 20 | ], 21 | reporters: ['progress'], 22 | port: 8123, 23 | singleRun: false, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front-end-js-testing", 3 | "version": "0.0.1", 4 | "description": "Starter project to demonstrate webdriver.io", 5 | "main": "index.js", 6 | "scripts": { 7 | "global-install": "npm i --global selenium-standalone@5.0.0 http-server@0.9.0 && selenium-standalone install", 8 | "server": "http-server ./static -p 8888 -c-1", 9 | "build": "webpack --config webpack.conf.js", 10 | "build-dev": "npm run server & webpack --config webpack.conf.js --watch", 11 | "start-selenium-server": "selenium-standalone start", 12 | "test": "npm run integration-test && npm run unit-test", 13 | "unit-test": "karma start karma.conf.js --single-run --no-auto-watch", 14 | "unit-test-background": "karma start karma.conf.js", 15 | "integration-test": "wdio ./wdio.conf.js" 16 | }, 17 | "dependencies": { 18 | }, 19 | "devDependencies": { 20 | "chai": "^3.5.0", 21 | "chai-as-promised": "^5.2.0", 22 | "karma": "^0.13.8", 23 | "karma-chrome-launcher": "^0.2.0", 24 | "karma-mocha": "^0.2.0", 25 | "karma-sinon-chai": "^1.0.0", 26 | "karma-webpack": "^1.7.0", 27 | "mocha": "^2.4.5", 28 | "sinon": "^1.17.2", 29 | "sinon-chai": "^2.8.0", 30 | "wdio-mocha-framework": "^0.2.11", 31 | "webdriverio": "^3.4.0", 32 | "webpack": "^1.12.14" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/bguiz/front-end-js-testing.git" 37 | }, 38 | "keywords": [ 39 | "webdriver", 40 | "webdriverio", 41 | "selenium", 42 | "integration", 43 | "testing" 44 | ], 45 | "author": "bguiz", 46 | "license": "GPL-3.0", 47 | "bugs": { 48 | "url": "https://github.com/bguiz/front-end-js-testing/issues" 49 | }, 50 | "homepage": "https://github.com/bguiz/front-end-js-testing#readme" 51 | } 52 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const functionToTest = require('./function-to-test.js'); 4 | 5 | document.addEventListener('DOMContentLoaded', () => { 6 | var pressMeButton = document.querySelector('.press-me'); 7 | var doNotPressMeButton = document.querySelector('.do-not-press-me'); 8 | var outputAreaSpan = document.querySelector('.output-area'); 9 | [ 10 | { button: pressMeButton, value: true, }, 11 | { button: doNotPressMeButton, value: false, } 12 | ].forEach((input) => { 13 | input.button.addEventListener('click', buttonPress.bind(undefined, input.value)); 14 | }); 15 | 16 | function buttonPress(shouldPass) { 17 | functionToTest(shouldPass) 18 | .then((value) => { 19 | outputAreaSpan.innerHTML = `Resolved: ${value}`; 20 | }) 21 | .catch((err) => { 22 | outputAreaSpan.innerHTML = `Rejected: ${err}`; 23 | }); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/function-to-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = functionToTest; 4 | 5 | function functionToTest(shouldPass) { 6 | return new Promise((resolve, reject) => { 7 | setTimeout(() => { 8 | if (shouldPass) { 9 | return resolve('Good'); 10 | } 11 | else { 12 | return reject('Bad'); 13 | } 14 | }, 10); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bguiz/front-end-js-testing/f7ff96033c7dee8b57fe5a0b6d30d5ab06f6b876/static/.gitkeep -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Front-end Javascript Testing 4 | 5 | 6 | 7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /test/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bguiz/front-end-js-testing/f7ff96033c7dee8b57fe5a0b6d30d5ab06f6b876/test/integration/.gitkeep -------------------------------------------------------------------------------- /test/integration/sample-integration.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | describe('[main page]', function() { 7 | it('should have a title', function() { 8 | return expect(browser.url('/').getTitle()) 9 | .to.eventually.be.equal('Front-end Javascript Testing'); 10 | }); 11 | 12 | it('should have a press me button', function() { 13 | return expect(browser.getText('.press-me')) 14 | .to.eventually.be.equal('Press Me'); 15 | }); 16 | 17 | it('should have a press me button', function() { 18 | return expect(browser.getText('.press-me')) 19 | .to.eventually.be.equal('Press Me'); 20 | }); 21 | 22 | it('should have an output area which is intially empty', function() { 23 | return expect(browser.getText('.output-area')) 24 | .to.eventually.be.equal(''); 25 | }); 26 | 27 | it('should click the press me button', function() { 28 | return browser.click('.press-me'); 29 | }); 30 | 31 | it('should have an output area with resolved text', function() { 32 | return expect(browser.getText('.output-area')) 33 | .to.eventually.be.equal('Resolved: Good'); 34 | }); 35 | 36 | it('should have a do not press me button', function() { 37 | return expect(browser.getText('.do-not-press-me')) 38 | .to.eventually.be.equal('Do Not Press Me'); 39 | }); 40 | 41 | it('should click the do not press me button', function() { 42 | return browser.click('.do-not-press-me'); 43 | }); 44 | 45 | it('should have an output area with rejected text', function() { 46 | return expect(browser.getText('.output-area')) 47 | .to.eventually.be.equal('Rejected: Bad'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/out/screens/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bguiz/front-end-js-testing/f7ff96033c7dee8b57fe5a0b6d30d5ab06f6b876/test/out/screens/.gitkeep -------------------------------------------------------------------------------- /test/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bguiz/front-end-js-testing/f7ff96033c7dee8b57fe5a0b6d30d5ab06f6b876/test/unit/.gitkeep -------------------------------------------------------------------------------- /test/unit/sample-unit.spec.js: -------------------------------------------------------------------------------- 1 | const chaiAsPromised = require('chai-as-promised'); 2 | chai.use(chaiAsPromised); 3 | 4 | const functionToTest = require('../../src/function-to-test.js'); 5 | 6 | describe('[sample unit]', function() { 7 | it('should pass functionToTest with true input', function() { 8 | return expect(functionToTest(true)) 9 | .to.eventually.be.equal('Good'); 10 | }); 11 | 12 | it('should fail functionToTest with false input', function() { 13 | return expect(functionToTest(false)) 14 | .to.eventually.be.rejectedWith('Bad'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /wdio.conf.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiAsPromised = require('chai-as-promised'); 3 | 4 | exports.config = { 5 | 6 | // 7 | // ================== 8 | // Specify Test Files 9 | // ================== 10 | // Define which test specs should run. The pattern is relative to the directory 11 | // from which `wdio` was called. Notice that, if you are calling `wdio` from an 12 | // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working 13 | // directory is where your package.json resides, so `wdio` will be called from there. 14 | // 15 | specs: [ 16 | './test/integration/**/*.spec.js' 17 | ], 18 | // Patterns to exclude. 19 | exclude: [ 20 | // 'path/to/excluded/files' 21 | ], 22 | // 23 | // ============ 24 | // Capabilities 25 | // ============ 26 | // Define your capabilities here. WebdriverIO can run multiple capabilties at the same 27 | // time. Depending on the number of capabilities, WebdriverIO launches several test 28 | // sessions. Within your capabilities you can overwrite the spec and exclude option in 29 | // order to group specific specs to a specific capability. 30 | // 31 | // If you have trouble getting all important capabilities together, check out the 32 | // Sauce Labs platform configurator - a great tool to configure your capabilities: 33 | // https://docs.saucelabs.com/reference/platforms-configurator 34 | // 35 | capabilities: [{ 36 | browserName: 'chrome' 37 | }], 38 | // 39 | // =================== 40 | // Test Configurations 41 | // =================== 42 | // Define all options that are relevant for the WebdriverIO instance here 43 | // 44 | // Level of logging verbosity: silent | verbose | command | data | result | error 45 | logLevel: 'verbose', 46 | // 47 | // Enables colors for log output. 48 | coloredLogs: true, 49 | // 50 | // Saves a screenshot to a given path if a command fails. 51 | screenshotPath: './test/out/screens/', 52 | // 53 | // Set a base URL in order to shorten url command calls. If your url parameter starts 54 | // with "/", the base url gets prepended. 55 | baseUrl: 'http://localhost:8888', 56 | // 57 | // Default timeout for all waitForXXX commands. 58 | waitforTimeout: 10000, 59 | // 60 | // Default timeout in milliseconds for request 61 | // if Selenium Grid doesn't send response 62 | connectionRetryTimeout: 90000, 63 | // 64 | // Default request retries count 65 | connectionRetryCount: 3, 66 | // 67 | // Initialize the browser instance with a WebdriverIO plugin. The object should have the 68 | // plugin name as key and the desired plugin options as property. Make sure you have 69 | // the plugin installed before running any tests. The following plugins are currently 70 | // available: 71 | // WebdriverCSS: https://github.com/webdriverio/webdrivercss 72 | // WebdriverRTC: https://github.com/webdriverio/webdriverrtc 73 | // Browserevent: https://github.com/webdriverio/browserevent 74 | // plugins: { 75 | // webdrivercss: { 76 | // screenshotRoot: 'my-shots', 77 | // failedComparisonsRoot: 'diffs', 78 | // misMatchTolerance: 0.05, 79 | // screenWidth: [320,480,640,1024] 80 | // }, 81 | // webdriverrtc: {}, 82 | // browserevent: {} 83 | // }, 84 | // 85 | // Test runner services 86 | // Services take over a specfic job you don't want to take care of. They enhance 87 | // your test setup with almost no self effort. Unlike plugins they don't add new 88 | // commands but hook themself up into the test process. 89 | // services: [],// 90 | // Framework you want to run your specs with. 91 | // The following are supported: mocha, jasmine and cucumber 92 | // see also: http://webdriver.io/guide/testrunner/frameworks.html 93 | // 94 | // Make sure you have the wdio adapter package for the specific framework installed 95 | // before running any tests. 96 | framework: 'mocha', 97 | // 98 | // Test reporter for stdout. 99 | // The following are supported: dot (default), spec and xunit 100 | // see also: http://webdriver.io/guide/testrunner/reporters.html 101 | // reporters: ['dot'], 102 | // 103 | // Options to be passed to Mocha. 104 | // See the full list at http://mochajs.org/ 105 | mochaOpts: { 106 | ui: 'bdd' 107 | }, 108 | // 109 | // ===== 110 | // Hooks 111 | // ===== 112 | // WedriverIO provides a several hooks you can use to intefere the test process in order to enhance 113 | // it and build services around it. You can either apply a single function to it or an array of 114 | // methods. If one of them returns with a promise, WebdriverIO will wait until that promise got 115 | // resolved to continue. 116 | // 117 | // Gets executed once before all workers get launched. 118 | // onPrepare: function (config, capabilities) { 119 | // }, 120 | // 121 | // Gets executed before test execution begins. At this point you can access to all global 122 | // variables like `browser`. It is the perfect place to define custom commands. 123 | before: function (capabilties, specs) { 124 | chai.Should(); 125 | chai.use(chaiAsPromised); 126 | chaiAsPromised.transferPromiseness = browser.transferPromiseness; 127 | }, 128 | 129 | // Hook that gets executed before the suite starts 130 | // beforeSuite: function (suite) { 131 | // }, 132 | // 133 | // Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling 134 | // beforeEach in Mocha) 135 | // beforeHook: function () { 136 | // }, 137 | // 138 | // Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling 139 | // afterEach in Mocha) 140 | // afterHook: function () { 141 | // }, 142 | // 143 | // Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts. 144 | // beforeTest: function (test) { 145 | // }, 146 | // 147 | // Runs before a WebdriverIO command gets executed. 148 | // beforeCommand: function (commandName, args) { 149 | // }, 150 | // 151 | // Runs after a WebdriverIO command gets executed 152 | // afterCommand: function (commandName, args, result, error) { 153 | // }, 154 | // 155 | // Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts. 156 | // afterTest: function (test) { 157 | // }, 158 | // 159 | // Hook that gets executed after the suite has ended 160 | // afterSuite: function (suite) { 161 | // }, 162 | // 163 | // Gets executed after all tests are done. You still have access to all global variables from 164 | // the test. 165 | // after: function (capabilties, specs) { 166 | // }, 167 | // 168 | // Gets executed after all workers got shut down and the process is about to exit. It is not 169 | // possible to defer the end of the process using a promise. 170 | // onComplete: function(exitCode) { 171 | // } 172 | } 173 | -------------------------------------------------------------------------------- /webpack.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var cwd = __dirname; 5 | 6 | module.exports = { 7 | entry: './src/client.js', 8 | output: { 9 | filename: './static/client.js', 10 | }, 11 | module: { 12 | loaders: [ 13 | { test: /\.css$/, loader: "style!css" } 14 | ] 15 | }, 16 | colors: true, 17 | devtool: 'source-map', 18 | }; 19 | --------------------------------------------------------------------------------