├── images ├── gbp-coins.jpg ├── jsdoc-html.png ├── build-passing.png ├── enable-travis-ci.png ├── first-failing-test.png ├── first-test-passes.png ├── showing-coverage.png ├── anatomy-of-a-unit-test.png ├── build-passing-summary.png ├── showing-all-passing-tests.png ├── showing-rogue-code-not-covered.png ├── initial-index-html-showing-failing-test.png ├── server-side-test-istanbul-coverage-report.png ├── server-side-command-line-test-run-with-istanbul.png ├── showing-rogue-code-on-one-line-goes-un-detected.png ├── server-side-test-istanbul-coverage-highlights-rogue-code.png └── server-side-command-line-test-run-with-istanbul-100-percent-coverage.png ├── CONTRIBUTING.md ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── package.json ├── change-functional.js ├── index.html ├── change.js ├── test.js ├── LICENSE └── README.md /images/gbp-coins.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/gbp-coins.jpg -------------------------------------------------------------------------------- /images/jsdoc-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/jsdoc-html.png -------------------------------------------------------------------------------- /images/build-passing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/build-passing.png -------------------------------------------------------------------------------- /images/enable-travis-ci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/enable-travis-ci.png -------------------------------------------------------------------------------- /images/first-failing-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/first-failing-test.png -------------------------------------------------------------------------------- /images/first-test-passes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/first-test-passes.png -------------------------------------------------------------------------------- /images/showing-coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/showing-coverage.png -------------------------------------------------------------------------------- /images/anatomy-of-a-unit-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/anatomy-of-a-unit-test.png -------------------------------------------------------------------------------- /images/build-passing-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/build-passing-summary.png -------------------------------------------------------------------------------- /images/showing-all-passing-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/showing-all-passing-tests.png -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | _**Please read** our_ 2 | [**contribution guide**](https://github.com/dwyl/contributing) 3 | (_thank you_!) 4 | 5 | -------------------------------------------------------------------------------- /images/showing-rogue-code-not-covered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/showing-rogue-code-not-covered.png -------------------------------------------------------------------------------- /images/initial-index-html-showing-failing-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/initial-index-html-showing-failing-test.png -------------------------------------------------------------------------------- /images/server-side-test-istanbul-coverage-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/server-side-test-istanbul-coverage-report.png -------------------------------------------------------------------------------- /images/server-side-command-line-test-run-with-istanbul.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/server-side-command-line-test-run-with-istanbul.png -------------------------------------------------------------------------------- /images/showing-rogue-code-on-one-line-goes-un-detected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/showing-rogue-code-on-one-line-goes-un-detected.png -------------------------------------------------------------------------------- /images/server-side-test-istanbul-coverage-highlights-rogue-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/server-side-test-istanbul-coverage-highlights-rogue-code.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "17:00" 8 | timezone: Europe/London 9 | -------------------------------------------------------------------------------- /images/server-side-command-line-test-run-with-istanbul-100-percent-coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwyl/learn-tdd/HEAD/images/server-side-command-line-test-run-with-istanbul-100-percent-coverage.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (https://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # exclude jsdoc output from Git see: https://github.com/docdis/learn-jsdoc 30 | out/ 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | # - run: npm run build --if-present 31 | - run: npm test 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v1 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-tdd", 3 | "version": "1.0.9", 4 | "description": "learn test driven development (TDD) using QUnit!", 5 | "main": "index.html", 6 | "scripts": { 7 | "test": "istanbul cover test.js", 8 | "open-coverage": "open ./coverage/lcov-report/index.html", 9 | "start": "live-server --port=8000", 10 | "docs": "jsdoc change.js", 11 | "open-docs": "open ./out/global.html" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/dwyl/learn-tdd.git" 16 | }, 17 | "keywords": [ 18 | "learn", 19 | "tdd", 20 | "testing", 21 | "tutorial", 22 | "beginner", 23 | "test driven development", 24 | "JavaScript", 25 | "qunit", 26 | "browser", 27 | "front-end" 28 | ], 29 | "author": "dwyl & friends!", 30 | "license": "ISC", 31 | "bugs": { 32 | "url": "https://github.com/dwyl/learn-tdd/issues" 33 | }, 34 | "homepage": "https://github.com/dwyl/learn-tdd", 35 | "devDependencies": { 36 | "istanbul": "^0.4.3", 37 | "jsdoc": "^4.0.0", 38 | "live-server": "^1.1.0", 39 | "qunit-tap": "^1.5.0", 40 | "qunitjs": "^2.0.0-rc1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /change-functional.js: -------------------------------------------------------------------------------- 1 | const COINS = [200, 100, 50, 20, 10, 5, 2, 1]; // "constant" of all coins 2 | /** 3 | * getChange accepts two parameters (totalPayable and cashPaid) and calculates 4 | * the change in "coins" that needs to be returned. 5 | * @param {number} totalPayable the integer amount (in pennies) to be paid 6 | * @param {number} cashPaid the integer amount (in pennies) the person paid 7 | * @returns {array} list of coins we need to dispense to the person as change 8 | * @example 9 | * getChange(215, 300); // returns [50, 20, 10, 5] 10 | */ 11 | function getChange (payable, paid) { 12 | return COINS.reduce((change, coin) => { 13 | const change_sum = change.reduce((sum, coin) => sum + coin, 0); 14 | const remaining = paid - payable - change_sum; 15 | const times_coin_fits = Math.floor(remaining / coin); 16 | return change.concat(Array(times_coin_fits).fill(coin)); 17 | }, []); // change array starts out empty and gets filled itteratively. 18 | } 19 | 20 | 21 | /* The code block below ONLY Applies to Node.js - This Demonstrates 22 | re-useability of JS code in both Back-end and Front-end! */ 23 | /* istanbul ignore next */ 24 | if (typeof module !== 'undefined' && module.exports) { 25 | module.exports = getChange; // allows CommonJS/Node.js require() / import 26 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vending Machine Change Calculator TDD Tutorial 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Vending Machine Change Calculator

14 |

Calculate the change (coins) to return to a customer when they buy something.

15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /change.js: -------------------------------------------------------------------------------- 1 | var coins = [200, 100, 50, 20, 10, 5, 2, 1]; 2 | /** 3 | * getChange accepts two parameters (totalPayable and cashPaid) and calculates 4 | * the change in "coins" that needs to be returned. 5 | * @param {number} payable the integer amount (in pennies) payable (to be paid) 6 | * @param {number} paid the integer amount (in pennies) the person paid 7 | * @returns {array} change the list of coins we need to dispense to the person 8 | * @example 9 | * getChange(215, 300); // returns [50, 20, 10, 5] 10 | */ 11 | function getChange (payable, paid) { 12 | var change = []; 13 | var length = coins.length; 14 | var remaining = paid - payable; // we reduce this below 15 | 16 | for (var i = 0; i < length; i++) { // loop through array of notes & coins: 17 | var coin = coins[i]; 18 | 19 | var times_coin_fits = Math.floor(remaining / coin); // no partial coins 20 | if(times_coin_fits >= 1) { // check coin fits into the remaining amount 21 | 22 | for(var j = 0; j < times_coin_fits ; j++) { // add coin to change x times 23 | change.push(coin); 24 | remaining = remaining - coin; // subtract coin from remaining 25 | } 26 | } 27 | } 28 | return change; 29 | }; 30 | 31 | /* The code block below ONLY Applies to Node.js - This Demonstrates 32 | re-useability of JS code in both Back-end and Front-end! #isomorphic */ 33 | /* istanbul ignore next */ 34 | if (typeof module !== 'undefined' && module.exports) { 35 | module.exports = getChange; // allows CommonJS/Node.js require() 36 | } 37 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | if (typeof module !== 'undefined' && module.exports) { // check we're server-side 3 | var QUnit = require('qunitjs'); // require QUnit node.js module 4 | // alias the QUnit.test method so we don't have to change all our tests 5 | var test = QUnit.test; // stores a copy of QUnit.test 6 | require('qunit-tap')(QUnit, console.log); // use console.log for test output 7 | var getChange = require('./change.js'); // load our getChange method 8 | } 9 | 10 | test('getChange(1,1) should equal [] - an empty array', function(assert) { 11 | var result = getChange(1, 1); //no change/coins just an empty array 12 | var expected = []; 13 | assert.deepEqual(result, expected); 14 | }); 15 | 16 | test('getChange(215, 300) should return [50, 20, 10, 5]', function(assert) { 17 | var result = getChange(215, 300); // expect an array containing [50,20,10,5] 18 | var expected = [50, 20, 10, 5]; 19 | assert.deepEqual(result, expected); 20 | }); 21 | 22 | test('getChange(486, 600) should equal [100, 10, 2, 2]', function(assert) { 23 | var result = getChange(486, 600); 24 | var expected = [100, 10, 2, 2]; 25 | assert.deepEqual(result, expected); 26 | }); 27 | 28 | test('getChange(12, 400) should return [200, 100, 50, 20, 10, 5, 2, 1]', function(assert) { 29 | var result = getChange(12, 400); 30 | var expected = [200, 100, 50, 20, 10, 5, 2, 1]; 31 | assert.deepEqual(result, expected); 32 | }); 33 | 34 | /* istanbul ignore next */ 35 | if (typeof module !== 'undefined' && module.exports) { QUnit.load(); } // run the tests 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | https://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. 363 | 364 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Learn **T**est **D**riven **D**evelopment (**TDD**) 4 | 5 | A brief introduction to **T**est **D**riven **D**evelopment (**TDD**) 6 | in JavaScript for people who want to write _**more reliable code**_. 7 | 8 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/learn-tdd/ci.yml?label=build&style=flat-square&branch=main)](https://github.com/dwyl/learn-tdd/actions/workflows/ci.yml) 9 | [![codecov.io](https://img.shields.io/codecov/c/github/dwyl/learn-tdd/master.svg?style=flat-square)](https://codecov.io/github/dwyl/learn-tdd?branch=master) 10 | [![Dependencies: None](https://img.shields.io/badge/dependencies-none-brightgreen.svg?style=flat-square)](https://github.com/dwyl/learn-tdd/blob/main/package.json#L35 "Zero Dependencies") 11 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/learn-tdd/issues) 12 | [![HitCount](https://hits.dwyl.com/dwyl/learn-tdd.svg)](https://hits.dwyl.com/dwyl/learn-tdd) 13 | 14 |
15 | 16 | ## Why? 17 | 18 | _Project(s) without tests_ often end up looking like they are stuck together with _**duct tape**_ ... 19 | 20 | ![duct tape car fail](https://i.imgur.com/9cNriGK.jpg) 21 | 22 | Change _one_ part and _another_ stops working? "_Fixing_" one bug, creates another? 23 | 24 | Wouldn't you *prefer* it if everything was 25 | ***consistent*** and beautifully integrated?
26 | What if _everyone_ on your team worked _like **clock-work**_ in a disciplined order... like a _**Formula 1 Crew**_ ... 27 | 28 | ![formula 1 pit stop](https://i.imgur.com/0NDbaam.jpg) 29 | 30 | Test Driven Development (TDD) makes your team a well-oiled machine which means you can go _**faster**_. 31 | 32 | Once you have a ***suite*** of tests that run on every change, you will 33 | begin to develop a whole other level of ***confidence*** in your codebase 34 | and will discover a new freedom to be ***creative*** without fear of 35 | "*breaking*" anything unexpectedly; truly *game-changing*. 36 | 37 | 38 | ## What? 39 | 40 | This tutorial will help you get started with 41 | **T**est **D**riven **D**evelopment (**TDD**) *today*!
42 | In the next ***30 minutes*** you will learn _everything_1 43 | you need to write tests for your web project! 44 | 45 | ### Pre-Requisites 46 | 47 | + **A computer** with a web browser 48 | + **Internet access** to download the starter files 49 | + **30 minutes** of your time 50 | + **_Basic_ Programming Skills** (HTML & JavaScript) 51 | + (_**Optional**_) _**Bonus Levels requires**_ you to 52 | [_install_ **Node.js**](https://nodejs.org/download/) 53 | 54 | ### What is Software Testing? 55 | > Software testing is the process of evaluating a software item to detect differences between the expected output and the actual output. Testing assesses the quality of the product. Software testing is a process that should be done during the development process. In other words software testing is a verification and validation process. 56 | 57 | 58 | ### What is TDD? 59 | 60 | > Test-driven development (TDD) is an evolutionary approach to development 61 | which combines test-first development, where you write a test before you write 62 | just enough production code to fulfil that test, and refactoring. In other words, 63 | it’s one way to think through your requirements 64 | or design before you write your functional code. 65 | 66 | *From [Introduction to Test Driven Development (TDD)](https://agiledata.org/essays/tdd.html)* 67 | 68 | #### Further resources 69 | - Software Testing - https://en.wikipedia.org/wiki/Software_testing 70 | - "What is Software Testing" video (from 5:56 onwards) - https://youtu.be/UZy1Dj9JIg4?t=356 71 | - Video intro to Software Development Lifecycle (from 0:52 onwards): https://youtu.be/qMkV_TDdDeA?t=52 72 | - How to Write Clean, Testable Code - https://youtu.be/XcT4yYu_TTs (ignore the Java code focus on the general principles) 73 | + [What is software testing?](https://www.codeproject.com/Tips/351122/What-is-software-testing-What-are-the-different-ty) by _Rehman Zafar_ 74 | 75 | 76 | ## How? 77 | 78 | The *first* thing you need to *understand* 79 | is that writing code following TDD (*discipline*) 80 | is a (*slightly*) different approach from simply 81 | diving into solving the problem (*without a test*). 82 | 83 | When reading about TDD you will usually see the expression: 84 | "***Red, Green, Refactor***": 85 | 86 | ![TDD Cycle: Red, Green, Refactor](https://i.imgur.com/RQe2NQT.jpg) 87 | 88 | What this means is that TDD follows a **3-step process**: 89 | 90 | 1. ***Write a Failing Test*** - Understand the (user) 91 | requirements/story well enough to write a test for what you expect. 92 | (_the test should **fail** initially - hence it being "Red"_) 93 | 94 | 2. ***Make the (failing) Test Pass*** - Write (*only*) the code you need 95 | to make the (*failing*) test pass, while ensuring your existing/previous tests 96 | all still pass (*no regressions*). 97 | 98 | 3. ***Refactor the code you wrote*** take the time to tidy up the code 99 | *you* wrote to make it simpler 100 | (*for your future self or colleagues to understand*) 101 | before you need to ship the current feature, do it. 102 | 103 | > Thankfully, because you will have good tests, 104 | you don't _need_ to do any refactoring up-front, 105 | you can always do refactoring _later_ 106 | if performance bottlenecks are discovered. 107 | Most programming languages have very efficient compilers/interpreters 108 | that remove much of the need for refactoring. 109 | And if you use a linter your code will be naturally "tidy". 110 | 111 | To develop the *habit(s)* you will need to be successful with TDD 112 | (*and software engineering in general*) 113 | we need to ***write*** a ***test first*** (*and watch it fail*) 114 | and *then* write the code required to make the test pass. 115 | 116 | Writing a _**failing test**_, 117 | before writing the code may seem *counter-intuitive*, 118 | *time consuming* or even "*tedious*" at _**first**_. 119 | But we _urge_ you to think of it this way: 120 | 121 | > The ***test*** is the ***question*** you are asking
122 | > your code is the ***answer*** to the question.
123 | > By having a _clear_ question, you can always check 124 | > that your code is working,
125 | > because it _**consistently**_ 126 | > gives you the same answer(s) ... _no surprises_, 127 | even when you're working with a large, inter-dependent code base! 128 | 129 | ## Practical 130 | 131 | > _**Note**: This tutorial is meant to be a beginner-friendly introduction to TDD. 132 | The Vending Machine example is _intentionally_ simple 133 | so you can focus on the principles of testing. 134 | Once you understand the basics, 135 | we encourage you to follow our _complete_ Todo List Tutorial 136 | ([https://github.com/dwyl/**todo-list-javascript-tutorial**](https://github.com/dwyl/todo-list-javascript-tutorial)), 137 | which is a step-by-step guide to building an App 138 | following testing and documentation-first best practices._ 139 | 140 | ### Scenario: Vending Machine _Change Calculator_ Micro-Project 141 | 142 | ![vending machine](https://i.imgur.com/HbwMqDa.jpg) 143 | 144 | Imagine you are building a **Vending Machine** 145 | that allows people to buy any item it contains. 146 | The machine accepts coins and calculates the change 147 | to be returned to the customer, given the item **price** 148 | and the **cash** received. 149 | 150 | ### Single *File* App 151 | 152 | We can build the _entire_ "project" in a _**single file**_: `index.html` 153 | 154 | > _**Note**: In practice you want to split your JavaScript, 155 | CSS and HTML (Templates) into **separate** files, 156 | but for this example we are keeping everything in `index.html` for simplicity. 157 | If you make it to the "Bonus Levels" you will split things out!_ 158 | 159 | Create a directory on your computer called **vending-machine**: 160 | 161 | In your **terminal** type this command: 162 | ```sh 163 | mkdir vending-machine && cd vending-machine 164 | ``` 165 | (_This will create the directory and move you into it_) 166 | 167 | Next create a file called **index.html** e.g: `atom index.html` 168 | (which creates and opens the file in the [Atom text editor](https://atom.io/) 169 | if you have it installed) 170 | 171 | (_**Note**: The "atom" command is not installed by default. 172 | In the Atom menu bar there is a command named “Install Shell Commands” 173 | which installs a new command in your Terminal called "atom"._) 174 | 175 | Now copy-paste the following *sample code* into the newly created `index.html` file to get started: 176 | 177 | ```html 178 | 179 | 180 | 181 | Vending Machine Change Calculator TDD Tutorial 182 | 183 | 184 | 185 | 186 | 187 | 188 |
189 |

Vending Machine Change Calculator

190 |

Calculate the change (coins) to return to a customer when they buy something.

191 |
192 | 193 |
194 |
195 | 196 | 197 | 209 | 210 | 211 | ``` 212 | 213 | #### Open index.html in your Browser 214 | 215 | When you ***open*** `index.html` in your ***web browser*** 216 | you should expect to see something like this: (_without the annotation pointing out the qunit div, and the green and red annotations pointing out the Passing and Failing tests_) 217 | 218 | ![learn-tdd-initial-index-html-showing-failing-test](images/initial-index-html-showing-failing-test.png) 219 | 220 | 221 | 222 | ##### Explanation 223 | 224 | There is quite a lot of code in the **index.html** you just created, 225 | let's step through it to understand the parts: 226 | 227 | The first part of **index.html** is a standard HTML head and body: 228 | 229 | ```html 230 | 231 | 232 | 233 | Vending Machine Change Calculator TDD 234 | 235 | 236 | 237 | 238 | 239 | 240 |
241 |

Vending Machine Change Calculator

242 |

Calculate the Change for a Given Price and Cash Received

243 |
244 | ``` 245 | Nothing special here, we are simply setting up the page 246 | and loading the CSS files. 247 | 248 | Next we see the **qunit divs** 249 | (_where the **test results** will be **displayed**_) 250 | and load the JQuery and QUnit Libraries from CDN: 251 | 252 | ```html 253 |
254 |
255 | 256 | 257 | ``` 258 | 259 | Finally we see our test(s) - the interesting part of the file: 260 | 261 | ```html 262 | 276 | 277 | 278 | ``` 279 | 280 | If you are new to writing ***automated tests***, don't worry - 281 | they are really simple. There are **3 parts**: 282 | 283 | 1. **Description** - usually the *first* parameter to QUnit's test() method, describing what is expected to happen in the test 284 | 2. **Computation** - executes a function/method 285 | (*which invokes the method you will write to make your test pass*) 286 | 3. **Assertion** - verifies that the result of your computation 287 | is what you ***expect*** it to be. 288 | 289 | ![anatomy-of-a-unit-test](images/anatomy-of-a-unit-test.png) 290 | 291 | In the above screenshot, the assertion is `assert.equal(result, 2)`
292 | We are giving the `equal` method two arguments; the `result` of our computation 293 | and our expected value - in this case **2**. _That's it_. 294 | 295 | 296 | _**Note**_: 297 | The latest version of QUnit uses the `QUnit.test()` function to run tests. 298 | Later in this workshop we use [blanket.js](https://blanketjs.org/) 299 | which is not compatible with the latest 300 | version of QUnit. It is for this reason 301 | that we are calling `test()` to run the tests in this workshop. 302 | 303 | ##### Further Reading: 304 | 305 | + Test assertion: https://en.wikipedia.org/wiki/Test_assertion 306 | + What are Test Assertions and how do they work?: 307 | https://www.thoughtworks.com/insights/blog/test-assertions-how-do-they-work 308 | 309 | 310 | ## Requirements 311 | 312 | As a customer, I want to buy a selected item from the **vending machine** 313 | and see what my change is as a **result** into the various **coins** 314 | so that I can select one of the options and receive my change. 315 | 316 | Acceptance criteria: 317 | - A successful call of a function `getChange` should return 318 | the change value in the various **coins** available 319 | - Unit Tests should exist when the function is ready 320 | - The selection of the desired return is out of scope 321 | 322 | ##### Complementary User Story view 323 | > Given a **Price** and an amount of **Cash** from the Customer 324 | > Return: **Change** to the customer (*in notes and coins*). 325 | 326 | ### Understand what is needed 327 | 328 | + Create a `function` called `getChange` that accepts _**two parameters**_: 329 | `totalPayable` and `cashPaid` 330 | + For a given `totalPayable` 331 | (the total amount an item in the vending machine costs) 332 | and `cashPaid` (the amount of cash the customer paid into the vending machine), 333 | `getChange` should _**calculate**_ the _**change**_ 334 | the machine should _return_ to the customer 335 | + `getChange` should _**return**_ change as an `array` of coins (largest to smallest) 336 | that the vending machine will need to _dispense_ to the customer. 337 | 338 | #### _Example_ 339 | 340 | If a customer buys an item costing £2.15 341 | (_we represent this as **215 pennies**_ `totalPayable`) 342 | and pays £3 (3 x £1 or _**300 pennies**_ `cashPaid`) 343 | into the vending machine, the _**change**_ will be **85p**.
344 | To dispense the 85p of change we should _return_ 345 | **four coins** to the person: 50p, 20p, 10p and 5p.
346 | An **array** of these coins would look like: `[50, 20, 10, 5]` 347 | 348 | #### Coins 349 | 350 | In the UK we have the following Coins: 351 | 352 | ![GBP Coins](images/gbp-coins.jpg "GBP Coins") 353 | 354 | 355 | If we use the penny as the unit (i.e. 100 pennies in a pound) 356 | the coins can be represented as: 357 | 358 | - 200 (£2) 359 | - 100 (£1) 360 | - 50 (50p) 361 | - 20 (20p) 362 | - 10 (10p) 363 | - 5 (5p) 364 | - 2 (2p) 365 | - 1 (1p) 366 | 367 | this can be stored as an Array: 368 | 369 | ```javascript 370 | var coins = [200, 100, 50, 20, 10, 5, 2, 1]; 371 | ``` 372 | 373 | _**Note**_: The same can be done for any other cash system ($ ¥ €) 374 | simply use the cent, sen or rin as the unit and scale up notes. 375 | 376 | #### The First Test 377 | 378 | If you are *totally* new to TDD I recommend reading this 379 | [introductory article](https://www.agiledata.org/essays/tdd.html) by Scott Ambler 380 | (especially the diagrams) otherwise this (test-fail-code-pass) process 381 | may seem *strange* ... 382 | 383 | In **T**est **F**irst **D**evelopment (TFD) we write a test *first* and *then* 384 | write the code that makes the test pass. 385 | 386 | #### First Requirement 387 | 388 | So, back in our **index.html** file ***remove the dummy tests*** 389 | and add the following lines: 390 | 391 | ```js 392 | test('getChange(1,1) should equal [] - an empty array', function(assert) { 393 | var result = getChange(1, 1); //no change/coins just an empty array 394 | var expected = []; 395 | assert.deepEqual(result, expected); 396 | }); // use deepEqual for arrays see: https://api.qunitjs.com/deepEqual/ 397 | ``` 398 | We use QUnit's `deepEqual` (_assert_) method to check that all the _elements_ 399 | in the two arrays are _**identical**_. see: https://api.qunitjs.com/deepEqual/ 400 | 401 | At this point, your `index.html` file should look like this: 402 | 403 | ```html 404 | 405 | 406 | 407 | Vending Machine Change Calculator TDD 408 | 409 | 410 | 411 | 412 |
413 |

Vending Machine Change Calculator

414 |

Calculate the Change for a Given Price and Cash Received

415 |
416 | 417 |
418 |
419 | 420 | 421 | 424 | 425 | 433 | 434 | 435 | ``` 436 | 437 | 438 | #### Watch it _Fail_ 439 | 440 | Back in your browser window, _refresh_ the browser and watch it *fail*: 441 | 442 | ![first failing test](images/first-failing-test.png) 443 | 444 | > **Q**: Why *deliberately* write a test we *know* is going to *fail*...?
445 | > **A**: To get used to the idea of *only* writing the code required to *pass* 446 | > the *current* (*failing*) *test*.
447 | > *Read*: "***The Importance of Test Failure***: 448 | https://www.sustainabletdd.com/2012/03/importance-of-test-failure.html
449 | **Note**: This also proves the test **will** fail if the code doesn't behave as expected. 450 | 451 | #### Create the getChange `function` 452 | 453 | In your `index.html` file add the following code (*above the tests*) 454 | 455 | ```js 456 | 464 | ``` 465 | 466 | Your `index.html` should now look something like this: 467 | 468 | ```html 469 | 470 | 471 | 472 | Vending Machine Change Calculator TDD 473 | 474 | 475 | 476 | 477 |
478 |

Vending Machine Change Calculator

479 |

Calculate the Change for a Given Price and Cash Received

480 | 481 |
482 | 483 |
484 |
485 | 486 | 487 | 496 | 497 | 505 | 506 | 507 | ``` 508 | 509 | 510 | #### Refresh `index.html` in the Browser 511 | 512 | 513 | ![first test passes](images/first-test-passes.png) 514 | 515 | It Passed!! 516 | 517 | #### Now Let's Write A *Real* Test 518 | 519 | Going back to the requirements, we need our `getChange` method to accept 520 | two arguments/parameters (`totalPayable` and `cashPaid`), and to `return` an 521 | `array` containing the coins equal to the difference between them: 522 | 523 | e.g: 524 | ```js 525 | totalPayable = 215 // £2.15 526 | cashPaid = 300 // £3.00 527 | difference = 85 // 85p 528 | change = [50,20,10,5] // 50p, 20p, 10p, 5p 529 | ``` 530 | 531 | Add the following test to tests section of `index.html`: 532 | 533 | ```javascript 534 | test('getChange(215, 300) should return [50, 20, 10, 5]', function(assert) { 535 | var result = getChange(215, 300); // expect an array containing [50,20,10,5] 536 | var expected = [50, 20, 10, 5]; 537 | assert.deepEqual(result, expected); 538 | }) 539 | ``` 540 | 541 | #### Write the Method to Pass the Test 542 | 543 | What if I _**cheat**_ and make `getChange` return the expected result? 544 | 545 | ```javascript 546 | function getChange (totalPayable, cashPaid) { 547 | 'use strict'; 548 | 549 | var change = [50, 20, 10, 5]; // just "enough to pass the failing test" 550 | 551 | return change; 552 | }; 553 | ``` 554 | 555 | This will _pass_ the new test, but it also introduces a regression. The original 556 | test `getChange(1,1) should equal [] - an empty array` is now failing. 557 | 558 | Step 2 of the **TDD** process requires that *all* tests should pass, not just the 559 | newly added one. 560 | 561 | The `getChange` function needs to cater for two scenarios; when change should be returned and when it shouldn't. A new implementation of `getChange` that 562 | handles both scenarios could be: 563 | 564 | ```javascript 565 | function getChange (totalPayable, cashPaid) { 566 | 'use strict'; 567 | 568 | var change = []; 569 | 570 | if((cashPaid - totalPayable) != 0) { // Is any change required? 571 | change = [50, 20, 10, 5]; // just "enough to pass the failing test" 572 | } 573 | 574 | return change; 575 | }; 576 | ``` 577 | 578 | The regression has been fixed and all tests _pass_, but you have *hard coded* 579 | the result (*not exactly useful for a calculator...*) 580 | 581 | This only works *once*. When the Spec (Test) Writer writes the next test, 582 | the method will need to be re-written to satisfy it. 583 | 584 | Let's try it. Work out what you expect so you can write your test: 585 | ```js 586 | totalPayable = 486 // £4.86 587 | cashPaid = 600 // £6.00 588 | difference = 114 // £1.14 589 | change = [100,10,2,2] // £1, 10p, 2p, 2p 590 | ``` 591 | 592 | Add the following test to `index.html` and refresh your browser: 593 | 594 | ```javascript 595 | test('getChange(486, 600) should equal [100, 10, 2, 2]', function(assert) { 596 | var result = getChange(486, 600); 597 | var expected = [100, 10, 2, 2]; 598 | assert.deepEqual(result, expected); 599 | }) 600 | ``` 601 | 602 | 603 | #### Should We _Keep Cheating or Solve the Problem_? 604 | 605 | We could _keep cheating_ by writing a series of if statements: 606 | 607 | ```javascript 608 | function getChange (totalPayable, cashPaid) { 609 | 'use strict'; 610 | 611 | var change = []; 612 | 613 | if((cashPaid - totalPayable) != 0) { // Is any change required? 614 | if(totalPayable == 486 && cashPaid == 600) 615 | change = [100, 10, 2, 2]; 616 | else if(totalPayable == 215 && cashPaid == 300) 617 | change = [50, 20, 10, 5]; 618 | } 619 | 620 | return change; 621 | }; 622 | ``` 623 | The _**Arthur Andersen Approach**_ gets results in the *short run* ... 624 | 625 | But it's arguably *more work* than simply *solving* the problem. 626 | So let's do that instead. 627 | 628 | # Try It Yourself (_before looking at the solution_!) 629 | 630 | > Try to create your own `getChange` method that passes the three tests 631 | > _before_ you look at the solution... 632 | 633 | To re-cap, these are our three tests: 634 | ```js 635 | test('getChange(1,1) should equal [] - an empty array', function(assert) { 636 | var result = getChange(1, 1); //no change/coins just an empty array 637 | var expected = []; 638 | assert.deepEqual(result, expected); 639 | }); 640 | 641 | test('getChange(215, 300) should return [50, 20, 10, 5]', function(assert) { 642 | var result = getChange(215, 300); // expect an array containing [50,20,10,5] 643 | var expected = [50, 20, 10, 5]; 644 | assert.deepEqual(result, expected); 645 | }); 646 | 647 | test('getChange(486, 600) should equal [100, 10, 2, 2]', function(assert) { 648 | var result = getChange(486, 600); 649 | var expected = [100, 10, 2, 2]; 650 | assert.deepEqual(result, expected); 651 | }); 652 | ``` 653 | 654 | #### One More Test to be _Sure_ it Works? 655 | 656 | Let's invent a test that will return one of each of the coins ... 657 | 658 | Recall that we have 8 types of coins: 659 | 660 | ```javascript 661 | var coins = [200, 100, 50, 20, 10, 5, 2, 1]; 662 | ``` 663 | 664 | The sum of the (_`array` containing one of each_) coins is: **388**p 665 | 666 | So, we need to create a test in which we **pay £4** for an item costing 12p. 667 | (A bit unrealistic, but if it works we know our `getChange` method is _ready_!) 668 | 669 | ```js 670 | test('getChange(12, 400) should return [200, 100, 50, 20, 10, 5, 2, 1]', function(assert) { 671 | var result = getChange(12, 400); 672 | var expected = [200, 100, 50, 20, 10, 5, 2, 1]; 673 | assert.deepEqual(result, expected); 674 | }); 675 | ``` 676 | 677 | When these tests pass, your work is done. 678 | 679 | 680 |
681 |
682 | 683 | ## Solution(s) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/learn-tdd/issues) 684 | 685 | _**Note**_: Feel free to suggest a more _compact_ algorithm. 686 | 687 | ### "Imperative" Version ("Two For Loops") 688 | 689 | ```javascript 690 | var coins = [200, 100, 50, 20, 10, 5, 2, 1] 691 | function getChange (payable, paid) { 692 | var change = []; 693 | var length = coins.length; 694 | var remaining = paid - payable; // we reduce this below 695 | 696 | for (var i = 0; i < length; i++) { // loop through array of notes & coins: 697 | var coin = coins[i]; 698 | 699 | var times_coin_fits = Math.floor(remaining / coin); // no partial coins 700 | if(times_coin_fits >= 1) { // check coin fits into the remaining amount 701 | 702 | for(var j = 0; j < times_coin_fits ; j++) { // add coin to change x times 703 | change.push(coin); 704 | remaining = remaining - coin; // subtract coin from remaining 705 | } 706 | } 707 | } 708 | return change; 709 | }; 710 | ``` 711 | 712 | ### "Functional" 713 | 714 | The "functional" solution is more _compact_ than the "nested for loops":
715 | 716 | ```js 717 | const COINS = [200, 100, 50, 20, 10, 5, 2, 1]; // "constant" of all coins 718 | function getChange (payable, paid) { 719 | return COINS.reduce((change, coin) => { 720 | const change_sum = change.reduce((sum, coin) => sum + coin, 0); 721 | const remaining = paid - payable - change_sum; 722 | const times_coin_fits = Math.floor(remaining / coin); 723 | return change.concat(Array(times_coin_fits).fill(coin)); 724 | }, []); // change array starts out empty and gets filled iteratively. 725 | } 726 | ``` 727 | 728 | Don't panic if you are _unfamiliar_ with the JavaScript 729 | `Array.Map` & `Array.Reduce` methods; 730 | they were new to everyone _once_. 731 | 732 | We recommend reading: 733 | 734 | + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Map 735 | + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce 736 | + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill 737 | + Example Functional JavaScript: https://www.sitepoint.com/map-reduce-functional-javascript 738 | + [Mostly Adequate Guide To Functional Programming](https://github.com/MostlyAdequate/mostly-adequate-guide) 739 | 740 | 741 | 742 | ### Alternative Solution 743 | 744 | An alternative shared by @blunket: 745 | 746 | ```javascript 747 | var cointypes = [200, 100, 50, 20, 10, 5, 2, 1]; 748 | function getChange(price, paid) { 749 | var difference = paid - price; 750 | var change = []; 751 | 752 | cointypes.forEach(function(coin) { 753 | // keep adding the current coin until it's more than the difference 754 | while (difference >= coin) { 755 | change.push(coin); 756 | difference = difference - coin; 757 | } 758 | }); 759 | 760 | return change; 761 | } 762 | ``` 763 | 764 | If you see this: 765 | 766 | ![learn-tdd-showing-all-passing-tests](images/showing-all-passing-tests.png) 767 | 768 | _**Congratulations! You can do Test Driven Development**_ (TDD)!!
769 | 770 | Give yourself a pat on the back! **Tweet your _success_**!
771 | _or **Re-Tweet**_: https://twitter.com/livelifelively/status/768645514120212480 772 | [![learn-tdd](https://cloud.githubusercontent.com/assets/194400/18021179/91827edc-6bdd-11e6-8ae5-082181c0d789.png)](https://twitter.com/livelifelively/status/768645514120212480) 773 | 774 | _Take a break, grab some water and come back for the #**BonusLevel**_ 775 | 776 |
777 | - - - 778 |
779 | 780 | 781 | ## Bonus Level 1: Code Coverage (10 mins) 782 | 783 | ### What is Code Coverage? 784 | 785 | ![sign not in use](https://i.imgur.com/bmY2imf.jpg) 786 | 787 | In computer programming, code coverage is a measure used to describe 788 | the degree to which the source code of a program is tested 789 | by a particular test suite. 790 | 791 | In other words: if there is code in the codebase which is not "_covered_" 792 | by a test, it could potentially be a source of bugs or undesirable behaviour. 793 | 794 | > Read more: https://en.wikipedia.org/wiki/Code_coverage 795 | 796 | ### Example from our Vending Machine _Coin Change_ Example 797 | 798 | Imagine the makers of the Vending Machine (_unknowingly_) 799 | hired a _**rogue**_ programmer to build the change calculator. 800 | 801 | The _**rogue**_ programmer charged below the "_market rate_", 802 | delivered the code quickly and even included tests! 803 | 804 | The makers of the vending machine think that everything is working fine, 805 | all the _tests_ pass and when they try the machine it dispenses the merchandise 806 | and the _correct change every time_. 807 | 808 | But in the `getChange` method the 809 | _**rogue**_ programmer put in the following lines: 810 | 811 | ```js 812 | if(cashPaid == 1337) { 813 | ATM = [20, 10, 5, 2]; 814 | for(var i = 0; i< 18; i++) { ATM.push(100) }; 815 | return ATM; } 816 | ``` 817 | 818 | If all the QA person did was run the tests they would see them 819 | all "green" and think the job was well done. 820 | 821 | But ... once the vending machines had gone into service, 822 | e.g: one in every train station in the country. 823 | The Vending Machine company begins to notice that there is less money 824 | in them than they expect ... They don't understand why because they only 825 | hire _trustworthy_ people to re-stock the machines. 826 | 827 | One day the Vending Machine Company decide to hire _you_ 828 | to review the code in the `getChange` calculator 829 | and you discover the _**rogue**_ programmer trick! 830 | 831 | Every time the _**rogue**_ programmer inserts £13.37 into _any_ 832 | vending machine it will payout £18.37 i.e: a **£5 payout** 833 | (and a "_free_" item from the vending machine!) 834 | 835 | > _How could this have been **prevented**_? 836 | 837 | The answer is ***code coverage***! 838 | 839 | > _Note: Checking code coverage is **not a substitute for QA/Code Review**..._! 840 | 841 | ### Blanket.js 842 | 843 | To check the _coverage_ of code being executed 844 | (_in the browser_) we use **Blanket.js** 845 | 846 | > See: https://blanketjs.org/ and https://github.com/alex-seville/blanket 847 | 848 | To *run* blanket.js we need to separate our tests and solution 849 | into distinct **.js** files: 850 | 851 | **test.js** contains our unit tests 852 | ```js 853 | test('getChange(1,1) should equal [] - an empty array', function(assert) { 854 | var result = getChange(1, 1); //no change/coins just an empty array 855 | var expected = []; 856 | assert.deepEqual(result, expected); 857 | }); 858 | 859 | test('getChange(215, 300) should return [50, 20, 10, 5]', function(assert) { 860 | var result = getChange(215, 300); // expect an array containing [50,20,10,5] 861 | var expected = [50, 20, 10, 5]; 862 | assert.deepEqual(result, expected); 863 | }); 864 | 865 | test('getChange(486, 600) should equal [100, 10, 2, 2]', function(assert) { 866 | var result = getChange(486, 600); 867 | var expected = [100, 10, 2, 2]; 868 | assert.deepEqual(result, expected); 869 | }); 870 | 871 | test('getChange(12, 400) should return [200, 100, 50, 20, 10, 5, 2, 1]', function(assert) { 872 | var result = getChange(12, 400); 873 | var expected = [200, 100, 50, 20, 10, 5, 2, 1]; 874 | assert.deepEqual(result, expected); 875 | }); 876 | ``` 877 | 878 | **change.js** has the `getChange` method. 879 | ```js 880 | var coins = [200, 100, 50, 20, 10, 5, 2, 1] 881 | function getChange(payable, paid) { 882 | var change = []; 883 | var length = coins.length; 884 | var remaining = paid - payable; // we reduce this below 885 | 886 | for (var i = 0; i < length; i++) { // loop through array of notes & coins: 887 | var coin = coins[i]; 888 | 889 | var times_coin_fits = Math.floor(remaining / coin); // no partial coins 890 | if(times_coin_fits >= 1) { // check coin fits into the remaining amount 891 | 892 | for(var j = 0; j < times_coin_fits; j++) { // add coin to change x times 893 | change.push(coin); 894 | remaining = remaining - coin; // subtract coin from remaining 895 | } 896 | } 897 | } 898 | if(paid == 1337) { 899 | ATM = [20, 10, 5, 2]; 900 | for(var i = 0; i< 18; i++) { ATM.push(100) }; 901 | return ATM; 902 | } 903 | else { 904 | return change; 905 | } 906 | }; 907 | ``` 908 | Include these two files _and_ the **Blanket.js** library in your index.html: 909 | ```html 910 | 911 | 912 | 913 | 914 | ``` 915 | 916 | ### _Live_ Server 917 | 918 | > _**Note**: This is a _light_ taste of Node.js for absolute beginners._ 919 | 920 | Because we are loading external **.js** files, our web browser will not _allow_ 921 | us to simply open the **index.html** from the directory. 922 | 923 | Open your terminal and run this command 924 | to _**install** the **node modules** and **start** the **live server**_: 925 | 926 | ```sh 927 | npm init -f && npm install live-server --save-dev && node_modules/.bin/live-server --port=8000 928 | ``` 929 | 930 | It will take a minute to install, 931 | but once that's done your `live-server` will start up. 932 | 933 | That starts a node.js HTTP server on port 8000. 934 | 935 | > Visit: http://localhost:8000/?coverage in your web browser 936 | 937 | You should expect to see: 938 | 939 | ![learn-tdd-showing-coverage](images/showing-coverage.png) 940 | 941 | (Make sure to tick "Enable Coverage", as it is not checked by default!) 942 | 943 | ### Click line #1 in the Blanket.js section to expand the code coverage view 944 | 945 | ![learn-tdd-showing-rogue-code-not-covered](images/showing-rogue-code-not-covered.png) 946 | 947 | Here we can clearly see which lines are **not** being covered by the tests! 948 | We can quickly identify a potential for bugs or _rogue_ code and remove it! 949 | 950 | #### Hold on ... What if the _rogue_ code is all on _one line_? 951 | 952 | 953 | 954 | ![learn-tdd-showing-rogue-code-on-one-line-goes-un-detected](images/showing-rogue-code-on-one-line-goes-un-detected.png) 955 | 956 | > The (_sad?_) _fact_ is: 957 | > Blanket.js Code Coverage analysis will not detect _all_ bugs or rogue code. 958 | > you **still need** a _**human**_ to do a _**code review**_! 959 | 960 | _But_ ... if you use _**Istanbul**_ to check coverage on the server, you'll see that only part of the single line of _rogue_ code was executed. 961 | Istanbul is _much_ better at spotting un-tested code! 962 | 963 | > We wrote a **beginners guide** to **Code Coverage with Istanbul**: 964 | [https://github.com/dwyl/**learn-istanbul**](https://github.com/dwyl/learn-istanbul) 965 | that goes into _detail_. 966 | 967 | ## Bonus Level 2: Node.js (_server-side_) Tests (10 mins) 968 | 969 | > _**Note**: You will need to have Node.js installed on your machine 970 | for this section. 971 | > If you don't already have it, download it from: 972 | https://nodejs.org/en/download/_ 973 | 974 | The beauty of writing JavaScript is that you can _**run**_ it _**anywhere**_! 975 | 976 | In this bonus level we are going 977 | to run our tests _**"server-side"**_ using **Node.js**. 978 | 979 | Add these lines to the top of the **test.js** file you 980 | created in **Bonus Level 1** 981 | 982 | ```js 983 | /* The code block below ONLY Applies to Node.js - This Demonstrates 984 | re-useability of JS code in both Back-end and Front-end! #isomorphic */ 985 | /* istanbul ignore if */ 986 | if (typeof module !== 'undefined' && module.exports) { 987 | var QUnit = require('qunitjs'); // require QUnit node.js module 988 | // alias the QUnit.test method so we don't have to change all our tests 989 | var test = QUnit.test; // stores a copy of QUnit.test 990 | require('qunit-tap')(QUnit, console.log); // use console.log for test output 991 | var getChange = require('./change.js'); // load our getChange method 992 | } 993 | ``` 994 | 995 | And add these lines to the bottom of the **test.js** file 996 | you created in **Bonus Level 1** 997 | 998 | ```js 999 | /* istanbul ignore next */ 1000 | if (typeof module !== 'undefined' && module.exports) { QUnit.load(); } // run the tests 1001 | ``` 1002 | 1003 | In addition, you need to add this to the **change.js** file you 1004 | created in **Bonus Level 1** 1005 | 1006 | ```js 1007 | /* The code block below ONLY Applies to Node.js - This Demonstrates 1008 | re-useability of JS code in both Back-end and Front-end! #isomorphic */ 1009 | /* istanbul ignore next */ 1010 | if (typeof module !== 'undefined' && module.exports) { 1011 | module.exports = getChange; // allows CommonJS/Node.js require() 1012 | } 1013 | ``` 1014 | 1015 | Next, install the following node.js modules by running `npm install qunitjs qunit-tap istanbul --save-dev`: 1016 | + QUnit node.js module 1017 | + qunit-tap (for command line output) 1018 | + Istanbul for server-side code coverage 1019 | 1020 | Run the tests in your _terminal_: 1021 | ```sh 1022 | node test.js 1023 | ``` 1024 | 1025 | And run Istanbul to see the server-side code coverage: 1026 | ```sh 1027 | ./node_modules/.bin/istanbul cover test.js 1028 | ``` 1029 | At this point, you should see something like this in your terminal: 1030 | 1031 | ![server-side-command-line-test-run-with-istanbul](images/server-side-command-line-test-run-with-istanbul.png) 1032 | 1033 | Execute `open ./coverage/lcov-report/index.html` to view the detailed coverage report, and you should see something like this: 1034 | 1035 | ![server-side-test-istanbul-coverage-highlights-rogue-code](images/server-side-test-istanbul-coverage-highlights-rogue-code.png) 1036 | 1037 | This clearly highlights the "*rogue*" code from the previous **Bonus Level**. 1038 | 1039 | Let's _remove_ the "_rogue_" code lines and re-run the tests: 1040 | 1041 | ![server-side-command-line-test-run-with-istanbul-100-percent-coverage](images/server-side-command-line-test-run-with-istanbul-100-percent-coverage.png) 1042 | 1043 | Refresh the Code Coverage report in your browser: 1044 | 1045 | ![server-side-test-istanbul-coverage-report](images/server-side-test-istanbul-coverage-report.png) 1046 | 1047 | > _**Boom**_! Now you know how to run your QUnit-based Unit Tests server-side! 1048 | 1049 | 1050 | ### *Top Tip*: Use [Codecov.io](https://codecov.io/#features) to Track Coverage in your Projects! 1051 | 1052 | > Now that you understand how Code Coverage Works, 1053 | > you can use https://codecov.io/#features 1054 | to ***track*** Coverage in your project over time! 1055 | > You can even add a [***Badge***](https://github.com/dwyl/repo-badges) 1056 | to your readme file e.g: 1057 | [![codecov.io](https://codecov.io/github/dwyl/learn-tdd/coverage.svg?branch=master)](https://codecov.io/github/dwyl/learn-tdd?branch=master) 1058 | to show others that you *care* about testing. 1059 | 1060 | ## Bonus Level 3: _Continuous Integration_ (5 mins) 1061 | 1062 | If you are new to **Continuous Integration** (**CI** _in general_) 1063 | or **Travis CI** check out our tutorial: 1064 | https://github.com/docdis/learn-travis 1065 | 1066 | To quickly add CI support to your project: 1067 | 1068 | **1**) **Visit**: https://travis-ci.org/profile and **Login** 1069 | with your **GitHub account**
1070 | **2**) Enable Travis for your project 1071 | (_**Note**_: The project will need to be hosted on GitHub) 1072 | 1073 | ![learn-tdd-enable-travis-ci](images/enable-travis-ci.png) 1074 | 1075 | **3**) Add a **.travis.yml** file to your project's root directory 1076 | and include the following lines in it: 1077 | 1078 | ```sh 1079 | language: node_js 1080 | node_js: 1081 | - "node" 1082 | ``` 1083 | **4**) Ensure that you have a **package.json** file 1084 | with **test** script.
1085 | (_if in doubt, just copy-paste the **package.json** from this project!_) 1086 | 1087 | **5**) **Commit** your changes and **push** them to GitHub
1088 | **6**) Visit the page on Travis-CI for your project. e.g: https://travis-ci.org/dwyl/learn-tdd 1089 | to see the build results. 1090 | 1091 | ![learn-tdd-build-passing-summary](images/build-passing-summary.png) 1092 | 1093 | ![learn-tdd-build-passing](images/build-passing.png) 1094 | 1095 | Done. [![Build Status](https://travis-ci.org/dwyl/learn-tdd.svg)](https://travis-ci.org/dwyl/learn-tdd) 1096 | 1097 |
1098 | 1099 | ## Bonus Level 4: _Documentation_ with [JSDoc](https://github.com/jsdoc3/jsdoc) (5 mins) 1100 | 1101 | > **Note**: Bonus Level 4 ***requires node.js*** to be *installed* on your machine. 1102 | > If you don't already have it installed, don't panic. You don't need to know 1103 | > *anything* about Node.js to work through the examples. 1104 | To download, visit: https://nodejs.org/en/download/ 1105 | and install the version for your Operating System. 1106 | 1107 | If you took a peak at the solution in **change.js** you may have noticed 1108 | that there is a ***comment block*** at the top of the file: 1109 | 1110 | ```js 1111 | /** 1112 | * getChange accepts two parameters (totalPayable and cashPaid) and calculates 1113 | * the change in "coins" that needs to be returned. 1114 | * @param {number} totalPayable the integer amount (in pennies) to be paid 1115 | * @param {number} cashPaid the integer amount (in pennies) the person paid 1116 | * @returns {array} list of coins we need to dispense to the person as change 1117 | * @example 1118 | * getChange(215, 300); // returns [50, 20, 10, 5] 1119 | */ 1120 | ``` 1121 | This is a JSDoc comment block which documents the `getChange` function/method. 1122 | 1123 | The beauty of writing documenting comments this way is that you can easily 1124 | produce documentation for your project in 3 easy steps: 1125 | 1126 | **1**) Install jsdoc: in your terminal run the following command 1127 | `npm install jsdoc --save-dev` 1128 | 1129 | **2**) Run the `jsdoc` command in your terminal: 1130 | `./node_modules/.bin/jsdoc change.js` 1131 | 1132 | **3**) Open the resulting **html** file 1133 | `open ./out/global.html#getChange` 1134 | and you should see something like this in your web browser: 1135 | 1136 | ![learn-tdd-jsdoc-html](images/jsdoc-html.png) 1137 | 1138 | This _clearly_ documents the functionality of the `getChange` method. 1139 | 1140 | - - - 1141 | 1142 | ## _Conclusion_ 1143 | 1144 | In the last **90 minutes** you _**learned how**_ to: 1145 | + Write code following **T**est **D**riven **D**evelopment (**TDD**) discipline 1146 | + Generate and view the **code coverage** for both front-end and back-end JavaScript Code 1147 | + Set up **Travis-CI Continuous Integration** for your project 1148 | (so that you can keep track of the test/build status for your project) 1149 | + Use **JSDoc** to generate documentation for your code after writing simple comment blocks 1150 | above your functions. 1151 | 1152 | > _Please **Star**_ this repository 1153 | and share it with your coder friends/colleagues.
1154 | > _Help us_ spread the TDD Love by ***re-tweeting***: 1155 | https://twitter.com/dwyl/status/621353373019865089 1156 | > If you have _**any questions**_ please ask: 1157 | > https://github.com/dwyl/learn-tdd/issues 1158 | 1159 | - - - 1160 | 1161 | 1Ok, its not *really* possible to learn "everything" in 30 mins... 1162 | but you'll certainly know *most* of what you need! 1163 | And, if you have *any questions*, _**please ask**_ at: 1164 | https://github.com/dwyl/learn-tdd/issues 1165 | 1166 |
1167 | 1168 | # What (_To Learn_) _Next_? 1169 | 1170 | Now that you know TDD basics, what should you learn/practice _next_...? 1171 | 1172 | + Learn ***Elm Architecture*** to build web applications 1173 | using the _**simple, reliable** and **fast**_ architecture 1174 | with our step-by-step guide: 1175 | [github.com/dwyl/**learn-elm-architecture**-in-javascript](https://github.com/dwyl/learn-elm-architecture-in-javascript) 1176 | This is relevant to anyone who wants to build Web or Mobile Apps using React.js 1177 | (_learning the principles of the **Elm Architecture** 1178 | will help to keep your code well-organised and with a logical rendering flow_) 1179 | + Learn ***Tape*** (_the simplest Node/Browser testing framework_): 1180 | https://github.com/dwyl/learn-tape 1181 | Apply your TDD knowledge to Node.js and browser testing 1182 | using the Tape framework which is both fast and flexible! 1183 | + Learn how to build a Todo List App (TodoMVC) in JavaScript from scratch: 1184 | https://github.com/dwyl/todo-list-javascript-tutorial 1185 | This is the _best_ way to practice your TDD skills by building a _real_ App 1186 | following TDD best-practice from start to finish. 1187 | This is also an extended example of using "Document Driven Development" 1188 | where all code is documented _before_ it is written using JSDoc comments. 1189 | 1190 | # Interested in Contributing? 1191 | _**Please read** our_ 1192 | [**contribution guide**](https://github.com/dwyl/contributing) 1193 | (_thank you_!) 1194 | --------------------------------------------------------------------------------