diff --git a/README.md b/README.md index 70d6dcb..a8a5f82 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,44 @@ -# Learn Tape -[![Build Status](https://travis-ci.org/dwyl/learn-tape.png?branch=master)](https://travis-ci.org/dwyl/learn-tape) -[![codecov](https://codecov.io/gh/dwyl/learn-tape/branch/master/graph/badge.svg)](https://codecov.io/gh/dwyl/learn-tape/branch/master) -[![Code Climate](https://codeclimate.com/github/dwyl/learn-tape.png)](https://codeclimate.com/github/dwyl/learn-tape) -[![devDependencies Status](https://david-dm.org/dwyl/learn-tape/dev-status.svg)](https://david-dm.org/dwyl/learn-tape?type=dev) -[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/learn-tape/issues) - -A *Beginner's Guide* to Test Driven Development (TDD) with ***Tape***. - -> **Note**: if you are ***new to Test Driven Development*** (TDD), we have a more *general* -***beginner's introduction*** and background about testing: -[https://github.com/dwyl/**learn-tdd**](https://github.com/dwyl/learn-tdd) +
+ +# Learn Tape ~ Testing in JavaScript + +[![Build Status](https://img.shields.io/travis/dwyl/learn-tape/master.svg?style=flat-square)](https://travis-ci.org/dwyl/learn-tape) +[![codecov.io](https://img.shields.io/codecov/c/github/dwyl/learn-tape/master.svg?style=flat-square)](http://codecov.io/github/dwyl/learn-tape?branch=master) +[![Code Climate](https://img.shields.io/codeclimate/maintainability/dwyl/learn-tape.svg?style=flat-square)](https://codeclimate.com/github/dwyl/learn-tape) +[![devDependencies Status](https://david-dm.org/dwyl/learn-tape/dev-status.svg?style=flat-square)](https://david-dm.org/dwyl/learn-tape?type=dev) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/learn-tape/issues) +[![HitCount](http://hits.dwyl.io/dwyl/learn-tape.svg)](http://hits.dwyl.io/dwyl/learn-tape) + + +A *Beginner's Guide* to Test Driven Development (TDD) using ***Tape*** +and ***Tap*** including front-end testing with JSDOM. + + + + Car Designers follow a Testing Mindset + + +
+
+ +> +**Note**: this guide is _specific_ to testing +with **`Tape`** and **`Tap`**.
+If you are ***new to Test Driven Development*** (TDD) in _general_, +consider reading our _**beginner's introduction**_: +[https://github.com/dwyl/**learn-tdd**](https://github.com/dwyl/learn-tdd) +
+The "vending machine" example/tutorial is _designed_ to be simple +for complete beginners.
+If you prefer a more _extended_ "real world" example app, see: +https://github.com/dwyl/todomvc-vanilla-javascript-elm-architecture-example +
+We _highly_ recommend learning the fundamentals here _first_ +before diving into the bigger example. Once you are comfortable +with the Tape/Tap syntax, there is a clear "next step". 📝✅ +
## *Why?* @@ -17,35 +46,38 @@ A *Beginner's Guide* to Test Driven Development (TDD) with ***Tape***. ***Testing*** your code is ***essential*** to ensuring reliability. There are _many_ testing frameworks so it can be -[*difficult to choose*](https://www.ted.com/talks/barry_schwartz_on_the_paradox_of_choice?language=en), -***most*** try to do too much, have ***too many features*** +[*difficult to choose*](https://www.ted.com/talks/barry_schwartz_on_the_paradox_of_choice?language=en). +Most testing frameworks/systems try to do too much, have ***too many features*** (["_bells and whistles_"](http://dictionary.cambridge.org/dictionary/english/bells-and-whistles) ...) or ***inject global variables*** into your run-time or have complicated syntax. -The _shortcut_ to choosing our tools is to apply the golden rule: +The _shortcut_ to choosing our tools is to apply Antoine's principal: -![perfection-achieved](https://cloud.githubusercontent.com/assets/194400/17927874/c7d06200-69ef-11e6-9ec8-a3c3692aaeed.png) +[![perfection-achieved](https://cloud.githubusercontent.com/assets/194400/17927874/c7d06200-69ef-11e6-9ec8-a3c3692aaeed.png "Perfection achieved when there is nothing left to take away ~ Antoine de Saint-Exupéry")](https://en.wikiquote.org/wiki/Antoine_de_Saint_Exup%C3%A9ry) -We use Tape because it's ***minimalist feature-set*** lets you craft ***simple maintainable tests*** that ***run fast***. +We use Tape because its ***minimalist feature-set*** +lets us craft ***simple maintainable tests*** that ***run fast***. -### _Reasons_ Why Tape (not XYZ Test Runner/Framework...) +### Why Tape (not XYZ Test Runner/Framework...)? -+ ***No configuration*** required. (_works out of the box, but can be configured if needed_) ++ ***No configuration*** required (_works out of the box, but can be configured if needed_). + ***No "Magic" / Global Variables*** injected into your run-time -(e.g: `describe`, `it`, `before`, `after`, etc.) -+ ***No Shared State*** between tests. (_tape does not encourage you to write messy / "leaky" tests_!) +(e.g: `describe`, `it`, `before`, `after`, etc.). ++ ***No Shared State*** between tests (_tape does not encourage you to write messy / "leaky" tests_!). + **Bare-minimum** only `require` or `import` into your test file. + Tests are **Just JavaScript** so you can run tests as a node script -e.g: `node test/my-test.js` +e.g: `node test/my-test.js`. + No globally installed "CLI" required to _run_ your tests. + Appearance of test output (what you see in your terminal/browser) is fully customisable. -> Read: https://medium.com/javascript-scene/why-i-use-tape-instead-of-Tape-so-should-you-6aa105d8eaf4 +> For more elaborate reasoning for using Tape, read:
https://medium.com/javascript-scene/why-i-use-tape-instead-of-Tape-so-should-you-6aa105d8eaf4 +
## *What?* -Tape is a JavaScript testing framework that works in both Node.js and Browsers. -It lets you write simple tests that are easy to read/maintain. +Tape is a JavaScript testing framework +that works in both Node.js and Browsers.
+It lets you write simple tests that are easy to read and maintain. The _output_ of Tape tests is a "***TAP Stream***" which can be read by other programs/packages e.g. to display statistics of your tests. @@ -59,22 +91,59 @@ https://en.wikipedia.org/wiki/Test_Anything_Protocol ## *Who?* -People who want to write tests for their Node.js or Web Browser JavaScript code. -(*i.e. ALL JavaScript coders!*) +People who write tests for their Node.js or Frontend JavaScript code. +(_i.e. everyone that writes JavaScript!_) ## *How?* + + +### _Initialise_ + +In your existing (_test-lacking_) project or a new learning directory,
+ensure that you have a **`package.json`** file +by running the **`npm init`** command: + +```sh +npm init -y +``` +That will create a basic **`package.json`** file with the following: +```js +{ + "name": "learn-tape", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} +``` +That's enough to continue with the learning quest. +We will update the `"scripts"` section later on.
+If you are _curious_ and want to _understand_ the **`package.json`** file +in more detail, see: https://docs.npmjs.com/files/package.json + +> If you are pushing your learning code to GitHub/GitLab, +consider adding a +[**`.gitignore`**](https://github.com/github/gitignore/blob/master/Node.gitignore) +file too. ### Install +Install **`tape`** using the following command: + ```sh npm install tape --save-dev ``` -(For us newbies, I'd like to suggest you include the npm init step. We did this project in Iron Yard bootcamp, but didn't know to init npm, nor to add Node to the .gitignore file) You should see some output *confirming* it *installed*: @@ -153,6 +222,16 @@ Great Succes! Let's try something with a bit more code. ### Mini TDD Project: Change Calculator +We are going to build a basic cash register change calculator +following TDD using tape. + +> +**Note**: this should be _familiar_ to you +if you followed the _general_ +[https://github.com/dwyl/**learn-tdd**](https://github.com/dwyl/learn-tdd) +tutorial. + + #### Basic Requirements > Given a **Total Payable** and **Cash From Customer** @@ -214,12 +293,14 @@ node test/change-calculator.test.js ![Tape TFD Fail](https://cloud.githubusercontent.com/assets/194400/18610249/3a620b70-7d0f-11e6-9af5-6176f2927b26.png "Tape TFD Fail = Cannot Find Module") -This error (``Cannot find module '../lib/change-calculator.js'`) is pretty self explanatory. +This error (`Cannot find module '../lib/change-calculator.js'`) +is pretty self explanatory.
We haven't created the file yet so the test is _requiring_ a non-existent file! > **Q**: Why *deliberately* write a test we *know* is going to *fail*...?
> **A**: To get used to the idea of *only* writing the code required to *pass* -> the *current* (*failing*) *test*, and _never_ write code you think you _might_ need. +> the *current* (*failing*) *test*, +and _never_ write code you think you _might_ need; see: [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it) @@ -230,9 +311,12 @@ write the code that makes the test pass. Create a new file for our change calculator `/lib/change-calculator.js`: -**Note**: We are *not* going to add any code to it _yet_. +> **Note**: We are *not* going to add any code to it _yet_. +This is _intentional_. -Re-run the test file in your terminal, you should expect to see _no output_ (_it will "pass" because there are no tests_) +Re-run the test file in your terminal, +you should expect to see _no output_ +(_it will "pass silently" because there are no tests!_) ![Tape Pass 0 Tests](https://cloud.githubusercontent.com/assets/194400/18610318/2d54ec02-7d11-11e6-8f72-35f967836348.png "Tape Pass 0 Tests") @@ -246,7 +330,7 @@ e.g: ``` totalPayable = 210 // £2.10 cashPaid = 300 // £3.00 -difference = 90 // 90p +difference = 90 // 90p change = [50,20,20] // 50p, 20p, 20p ``` @@ -269,8 +353,9 @@ Re-run the test file: `node test/change-calculator.test.js` #### Export the `calculateChange` Function -Right now our `change-calculator.js` file does not _contain_ anything, -so when it's `require`'d in the test we get a error: `TypeError: calculateChange is not a function` +Right now our `change-calculator.js` file does not _contain_ anything,
+so when it's `require`'d in the test we get a error: +`TypeError: calculateChange is not a function` We can "fix" this by _exporting_ a function. add a single line to `change-calculator.js`: @@ -297,7 +382,7 @@ Re-run the test file `node test/change-calculator.test.js` (_now it "passes"_): ![Tape 1 Test Passes](https://cloud.githubusercontent.com/assets/194400/18610825/877be2f0-7d1e-11e6-9e8f-887e9700fd1b.png "Tape 1 Test Passes") -> Note: we aren't _really_ ***passing*** the test, we are _faking_ it +> **Note**: we aren't _really_ ***passing*** the test, we are _faking_ it for illustration. @@ -340,10 +425,13 @@ module.exports = function calculateChange(totalPayable, cashPaid) { ``` But its arguably *more work* than simply *solving* the problem. -Lets do that instead. -(**Note**: this is the *readable* version of the solution! feel free to suggest a more compact algorithm) +Let's do that instead.
+ +> **Note**: this is the _readable_ version of the solution! + Feel free to suggest a + [more _compact_ function](https://github.com/dwyl/learn-tdd#solutions-). -Update the calculateChange function in `change-calculator.js`: +Update the `calculateChange` function in `change-calculator.js`: ```javascript module.exports = function calculateChange(totalPayable, cashPaid) { @@ -351,7 +439,7 @@ module.exports = function calculateChange(totalPayable, cashPaid) { const coins = [5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1]; let change = []; const length = coins.length; - let remaining = cashPaid - totalPayable; // we reduce this below + let remaining = cashPaid - totalPayable; // we reduce this below for (let i = 0; i < length; i++) { // loop through array of notes & coins: let coin = coins[i]; @@ -369,6 +457,18 @@ module.exports = function calculateChange(totalPayable, cashPaid) { }; ``` +> _**Note**: we **prefer** the "**functional programming**" approach +when solving the calculateChange function._
+_We have used an "**imperative**" style here simply because +it is more **familiar** to **most people** ...
+If you are **curious** about the the **functional** solution, +and you should be,
+see_: https://github.com/dwyl/learn-tdd#functional + + + +#### Add More Tests! + Add _one more_ test to ensure we are *fully* exercising our method: ``` @@ -390,10 +490,18 @@ test('calculateChange(1487,10000) should equal [5000, 2000, 1000, 500, 10, 2, 1 ![Tape 4 Passing](https://cloud.githubusercontent.com/assets/194400/18611450/9676bfb0-7d31-11e6-91fa-c48fb2630a65.png "Tape 4 Passing") +> _**Note**: adding more test examples is good way of achieving **confidence** +in your code. We often have 3x more example/test code than we do "library" code +in order to test all the "edge cases". +If you get the point where feel you are "working too hard" writing tests, +consider using_ +["property based testing"](https://github.com/dwyl/learn-elixir/issues/93#issuecomment-433616917) +_to **automate** testing thousands of cases_. + - - - -### Bonus Level +### _Bonus_ Level #### Code Coverage @@ -433,35 +541,47 @@ section in your `package.json`; ```sh istanbul cover tape ./test/*.test.js ``` + ### Run your Tape tests in the browser Follow these steps to run `Tape` tests in the browser: -1. You'll have to bundle up your test files so that the browser can read them. -We have chosen to use [`browserify`](https://www.npmjs.com/package/browserify) to do this. **(other module bundlers are -available)**. You'll need to install it globally to access the commands that -come with it. Enter the following command into the command line: +1. You'll have to bundle up your test files +so that the browser can read them.
+We have chosen to use [`browserify`](https://www.npmjs.com/package/browserify) to do this. (_other module bundlers are available_).
+You'll need to install it globally +to access the commands that come with it.
+Enter the following command into the command line: `npm install browserify --save-dev` + 2. Next you have to bundle your test files. Run the following browserify command: `node_modules/.bin/browserify test/*.js > lib/bundle.js` + 3. Create a `test.html` file that can hold your bundle: `touch lib/test.html` + 4. Add your test script to your newly created `test.html`: `echo '' > lib/test.html` -5. Copy the full path of your `test.html` file and then paste it into your -browser. Open up the developer console and you should see something that looks -like: + +5. Copy the full path of your `test.html` file +and then paste it into your browser.
+Open up the developer console +and you should see something that looks like this: + ![browser](https://cloud.githubusercontent.com/assets/12450298/19898078/79f41d30-a052-11e6-954b-8dad5fa71771.png) #### Headless Browser + You can print our your test results to the command line instead of the browser by using a headless browser: 1. Install [`testling`](https://www.npmjs.com/package/testling): `npm install testling --save-dev` + 2. Run the following command to print your test results in your terminal: `node_modules/.bin/browserify test/*.js | node_modules/.bin/testling` + 3. You should see something that looks like this: ![testling](https://cloud.githubusercontent.com/assets/12450298/19898553/63e0e8a0-a054-11e6-93e1-2fe4872989ed.png) @@ -470,19 +590,54 @@ by using a headless browser: > If you are new to Travis CI check out our tutorial: https://github.com/dwyl/learn-travis -Setting up Travis-CI (_or any other CI service_) for your Tape tests is _easy_ -simply define the `test` script in your `package.json`: +Setting up Travis-CI (_or any other CI service_) for your Tape tests +is quite straightforward.
+First define the `test` script in your `package.json`: -``` +```sh tape ./test/*.test.js ``` -We usually let Travis send Code Coverage data to Codecov.io so we run our -tape tests using Istanbul (see the coverage section above): +We usually let Travis send Code Coverage data to +[Codecov.io](https://github.com/dwyl/learn-istanbul#tracking-coverage-as-a-service) +so we run our tape tests using Istanbul (_see the coverage section above_): -``` +```sh istanbul cover tape ./test/*.test.js ``` -#### What about front end code with Tape? -Now that you're a pro at using Tape to test your back end code check out our [front end testing with tape guide!](https://github.com/dwyl/learn-tape/blob/master/front-end-with-tape.md) +Next add a basic `.travis.yml` file to your project: + +```yml +language: node_js +node_js: + - "node" +``` + +And _enable_ the project on Traivs-CI.
+**Done**. [![Build Status](https://img.shields.io/travis/dwyl/learn-tape/master.svg?style=flat-square)](https://travis-ci.org/dwyl/learn-tape) + +
+ + + +# Can We Use Tape for _Frontend_ Tests? + +Now that you've learned how to use Tape to test your back end code +check out our guide on +[frontend testing with tape](https://github.com/dwyl/learn-tape/blob/master/front-end-with-tape.md). + + +# What about _Tap_? + +We use **Tape** for _most_ of our JavaScript testing needs +[@dwyl](https://github.com/dwyl?language=javascript) +but _occasionally_ we find that having a few _specific_ extra functions +_simplifies_ our tests and reduces the repetitive "boilerplate". + +If you find yourself needing a **`before`** or **`after`** function +to do "setup", "teardown" or resetting state in tests, +***or*** you need to run tests in ***parallel*** +(_because you have lots of tests_), +then _consider_ using **`Tap`**: +[**`tap-advanced-testing.md`**](https://github.com/dwyl/learn-tape/blob/master/tap-advanced-testing.md) diff --git a/front-end-with-tape.md b/front-end-with-tape.md index e106232..1daba98 100644 --- a/front-end-with-tape.md +++ b/front-end-with-tape.md @@ -1,29 +1,39 @@ # Testing your front end Javascript with Tape ## Why? + Generally it would be nice to only use one testing framework rather than using -separate front end and back end testing frameworks. It means you don't have to flit -between different syntaxes for tests, and that you don't have to bloat your dev -dependencies +separate front end and back end testing frameworks. +It means you don't have to flit between different syntaxes for tests, +and that you don't have to bloat your `devDependencies`. ## What? + We're going to learn how to use JSDOM alongside tape to test our front end code. ## How? -We're going to have a small example, with some basic DOM manipulation, and we're -going to write some tests for it! +We're going to have a small example, with some basic DOM manipulation, +and we're going to write some tests for it! -If you haven't already, look through -[the readme of this repo](https://github.com/dwyl/learn-tape/blob/master/README.md) - to get introduced to tape. +> If you haven't already, look through the +[`README.md`](https://github.com/dwyl/learn-tape/blob/master/README.md) + of this repo to get introduced to tape. -So our example lives in `front-end-testing/lib`, just open the `index.html` in -that directory in your browser, it's a basic counter which increments, decrements - and resets. Have a look at it and read through the code in `script.js` and make - sure you understand it. +The example we are following is a basic counter +which increments, decrements and resets. +The complete code is in +[`front-end-testing/lib`](https://github.com/dwyl/learn-tape/tree/master/front-end-testing/lib), +open the [`index.html`](https://github.com/dwyl/learn-tape/blob/master/front-end-testing/lib/index.html) +in your web browser, -The first thing to note from this is the `if` statement at the bottom. +Have a look at it, read through the code in +[`script.js`](https://github.com/dwyl/learn-tape/blob/master/front-end-testing/lib/script.js) +and make sure you understand it. + +The first thing to note from reading +[`script.js`](https://github.com/dwyl/learn-tape/blob/master/front-end-testing/lib/script.js) +is the `if` statement at the bottom: ```js /*istanbul ignore next */ @@ -38,26 +48,33 @@ if (typeof module !== 'undefined' && module.exports) { } ``` -This just stops the browser from trying to process `module.exports` so that we -don't get any console errors when the code is run in actual browser -(if `module` is `undefined` then trying to access properties from it will result - in an error). +This just stops the browser from trying to process `module.exports` +so that we don't get any console errors +when the code is run in a browser +(if `module` is `undefined` + then trying to access properties from + it will result in an error). -We need to use `module.exports` still so that we can require it into our test file. +We need to use `module.exports` still +so that we can require it into our test file. -We use `/*istanbul ignore next*/` as we do not want this if statement to effect - our coverage. If you've not used istanbul before check out our +We use `/*istanbul ignore next*/` as we do +not want this if statement to effect our coverage.
+If you've not used istanbul before check out our [`learn-istanbul`](https://github.com/dwyl/learn-istanbul) repo. -So, lets get started! +With that `if` block of code covered, lets get started on testing! -Inside of the `front-end-testing` directory make a new directory called `test` +Inside of the +[`front-end-testing`](https://github.com/dwyl/learn-tape/tree/master/front-end-testing) +directory, make a new directory called `test` and inside of the new `front-end-testing/test` directory you just created create a file called `test.spec.js`. Open it in your favourite text editor and lets get started! First we'll require in tape, JSDOM and fs. + ```js const test = require('tape'); // jsdom is a way to create a document object on the backend, we only need the @@ -86,38 +103,47 @@ and we assign it to the variable 'DOM' const DOM = new JSDOM(html); ``` -Next we declare a global variable, node is a little different to the browser, +Next we declare a global variable. +Node is a little different to the browser: if we want something to be in the global scope (as in, available in other files whilst they are being processed by 'require') then we need to specifically -declare that. We do this with the 'document' from the DOM we just created so that - it can be used by our JS file. +declare that. We do this with the 'document' from the DOM we just created +so that it can be used by our JS file. ```js global.document = DOM.window.document; ``` -this takes the `document` from the DOM object and makes it globally available in -the current node environment, this means that when we require in our `script.js` -file it won't error due to `document` being undefined, because it is defined +:arrow_up: This takes the `document` from the DOM object +and makes it globally available +in the current node environment. +This means that when we require in our `script.js` +file it won't error due to `document` being undefined, +because it is defined with the DOM we just made! So now we can require our script file in. + ```js const frontEndCode = require('../lib/script.js'); ``` -So now frontEndCode will be an object which is a copy of what we exported using +Now frontEndCode will be an object +which is a copy of what we exported using `module.exports` in `script.js`. -So, now lets write some tests! +So, now let's write some tests! -Our increment and decrement functions both take in a number (or a number as a -string), and return that number increased/decreased by one, respectively. If it -is passed something which is not a number, or a string which can be coerced to a -number, then it should update the Dom to add an error message. +Our increment and decrement functions both take in a number +(or a number as a string), +and return that number increased/decreased by one, respectively. +If it is passed something which is not a number, +or a string which can be coerced to a number, +then it should update the DOM to add an error message. So first off lets write a test for increment for when it's passed the expected arguments. + ```js test('test increment function', function(t) { const actual = frontEndCode.increment(1); @@ -126,13 +152,15 @@ test('test increment function', function(t) { t.end(); }); ``` -This test is just like the tests from the README.md of this repo as we're doing -returning a basic value. -But, as said, if we call it with invalid input it updates the dom with with an -error message. +This test is just like the tests from the `README.md` +of this repo as we're returning a basic value. + +But, as said, if we call it with invalid input +it updates the DOM with an error message. So we can update the test function so it looks like this: + ```js test('test increment function', function(t) { let actual = frontEndCode.increment(1); @@ -140,35 +168,45 @@ test('test increment function', function(t) { t.equal(actual, expected, 'should add one to a number'); frontEndCode.increment('not a number'); // JSDOM does not support the use of 'node.innerText' so we have to use 'node.textContent' - // I can access things in the 'document' just like I would be able to in the browser + // I can access things in the 'document' just like in the browser actual = document.querySelector('.error').textContent; expected = 'Error: Argument passed to increment was not a number'; t.equal(actual, expected, 'should update error node when a string passed in'); t.end(); }); ``` -So let's break down what happens here: + +Let's break down what happens here: We call our increment function with invalid input + ```js frontEndCode.increment('not a number'); ``` -So we now hope that, as the code in our `script.js` suggests, the div with the + +We now hope that, as the code in our `script.js` suggests, the div with the class `error` should now have a text node inside which says "Error: Argument passed to increment was not a number". -So we can go get the `textContent` of that div and assign it to a variable, in -exactly the same way we would in front end code. +So we can go get the `textContent` of that div and assign it to a variable, +in exactly the same way we would in front end code. + ```js actual = document.querySelector('.error').textContent; + ``` -(since `let actual` has already been declared above in this function's scope, we -don't have to declare it, we just have to reassign it.) +(since `let actual` has already been declared above + in this function's scope, + we don't have to declare it, + we just have to reassign it.) Then assign what we expect to a variable: + ```js expected = 'Error: Argument passed to increment was not a number'; ``` + And then we compare them like any other test: + ```js t.equal(actual, expected, 'should update error node when a string passed in'); ``` @@ -181,35 +219,41 @@ You should see something which looks like this: ![picture of a terminal showing two passing tests](https://user-images.githubusercontent.com/21139983/27822341-4269c296-609d-11e7-928c-4d98dfbbfe7b.png) Now run: + ``` npm run front-end-coverage ``` + You should see something like this: + ![picture of test coverage](https://user-images.githubusercontent.com/21139983/27822333-3ab87a56-609d-11e7-8e33-905a5132f09e.png) ### Now you try! Our aim (as it always is) is to bring this up to 100% coverage! -Now that we've walked through how to write a test for the increment function, you -should now be able to have a go at writing tests for the `decrement`, `resetFunc`, -`currentCount`, and `updateDom` functions! +Now that we've walked through how to write a test for the increment function, +you should now be able to have a go at writing tests +for the `decrement`, `resetFunc`, `currentCount`, and `updateDom` functions! -If you get stuck you can take a look in the `front-end-testing/test-complete` to -see how we've written our tests. +If you get stuck you can take a look in the +[`front-end-testing/test-complete`](https://github.com/dwyl/learn-tape/tree/master/front-end-testing/test-complete) +to see how we've written our tests. Run `npm run front-end-test` whenever you write a test to make sure it's passing (don't forget to use `t.end()`). -When you've written tests for all of those functions run `npm run front-end-coverage` -, and you should see something like this: +When you've written tests for all of those functions, +run `npm run front-end-coverage` +and you should see something like this: + ![picture of a terminal showing increased coverage](https://user-images.githubusercontent.com/21139983/27823988-e61a302e-60a2-11e7-91ba-a8d8a3aa1a96.png) If your coverage is looking lower on any of the options then look at your tests, are you testing all of the functions? Are you testing all possible outcomes of each function? -### Let's get that coverage up to :100: +### Let's get that coverage up to 💯 The last bits to test are our event listeners, since inside of the functions passed as the arguments to the event listeners we compose our functions, we need @@ -221,26 +265,36 @@ passing unit tests for them, but now we want to test how they are put together. We can do this by simulating a `click` on the button! So, let's start with increment again, set up a new test just as we have been: + ```js test('increment is called properly when the inc button is clicked', function(t) { }); ``` -now we want to make sure that `count` is at zero, just in case the previous code -has effected it, so we pull in the count node and update it: +Now we want to make sure that `count` is at zero, +just in case the previous code has effected it, +so we pull in the count node and update it: + ```js let count = document.querySelector('.count'); frontEndCode.updateDom(frontEndCode.resetFunc(), count); ``` -Now, we want to simulate a click on our "+" button, just like in a browser we -can call `.click()` on any dom node to simulate a click on it! So we can add: +Next we want to simulate a click on our "+" button, +just like in a browser we can call `.click()` on any DOM node +to simulate a click on it! + +So we can add: + ```js document.querySelector('.inc').click(); ``` -And under that we can set up an expected, actual and a test by using our JSDOM. + +And under that we can set up an expected, +actual and a test by using our JSDOM. + ```js let actual = count.textContent; let expected = '1'; @@ -252,6 +306,7 @@ t.equal( t.end(); ``` You should now have a test that looks like this: + ```js test('increment is called properly when the inc button is clicked', function(t) { let count = document.querySelector('.count'); @@ -269,18 +324,27 @@ test('increment is called properly when the inc button is clicked', function(t) t.end(); }); ``` + run `npm run front-end-test` and watch the test pass. :tada: -Now, write a test for the event listener for the "-" and "reset" buttons, in a -really similar way to what we've just done. +Now, write a test for the event listener for the "-" and "reset" buttons, +in a really similar way to what we've just done. Once you've done that run `npm run front-end-coverage` again and bask in the glory of your 100% coverage! Just think of the badge on your repo when you get this on your own projects! -Now you can use tape for your front end and back end code, and get 100% test -coverage everywhere! +Now you can use tape for your front end and back end code, +and get 100% test coverage everywhere! + + +# _Now_ What? + +If you found this _taster_ to front-end testing with Tape and JSDOM +helpful, consider reading our more _comprehensive_ example: +https://github.com/dwyl/todomvc-vanilla-javascript-elm-architecture-example ## Further reading + + [JSDOM documentation](https://github.com/tmpvar/jsdom) + Testing online/offline functionality using JDOM [stack overflow answer](https://stackoverflow.com/questions/44678841/simulating-going-online-offline-with-jsdom) diff --git a/lib/change-calculator.js b/lib/change-calculator.js index f7e5f73..121c00f 100644 --- a/lib/change-calculator.js +++ b/lib/change-calculator.js @@ -1,37 +1,20 @@ -const coins = [5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1]; +const COINS = [5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1]; /** - * calculateChange accepts two parameters (totalPayable and cashPaid) and calculates - * the change in "coins" that needs to be returned. + * calculateChange accepts three parameters (totalPayable, cashPaid, coinsAvail) + * and calculates the change in "coins" that needs to be returned. * @param {number} totalPayable the integer amount (in pennies) to be paid * @param {number} cashPaid the integer amount (in pennies) the person paid + * @param {array} [coinsAvail=COINS] the list of coins to select change from. * @returns {array} list of coins we need to dispense to the person as change * @example calculateChange(215, 300); // returns [50, 20, 10, 5] */ - module.exports = function calculateChange(totalPayable, cashPaid) { - - let change = []; - const length = coins.length; - let remaining = cashPaid - totalPayable; // we reduce this below - - for (let i = 0; i < length; i++) { // loop through array of notes & coins: - let coin = coins[i]; - - if(remaining/coin >= 1) { // check coin fits into the remaining amount - let times = Math.floor(remaining/coin); // no partial coins - - for(let j = 0; j < times; j++) { // add coin to change x times - change.push(coin); - remaining = remaining - coin; // subtract coin from remaining - } - } - } - return change; - }; - -/* The code block below ONLY Applies to Node.js - This Demonstrates - re-useability of JS code in both Back-end and Front-end! #universal */ -/* istanbul ignore next */ -// if (typeof module !== 'undefined' && module.exports) { -// module.exports = getChange; // allows CommonJS/Node.js require() -// } +module.exports = function calculateChange (totalPayable, cashPaid, coinsAvail) { + const coins = coinsAvail || COINS; // if coinsAvail param undefined use COINS + return coins.reduce(function (change, coin) { + const sum = change.reduce(function (sum, coin) { return sum + coin }, 0); + const remaining = cashPaid - totalPayable - sum; + const times_coin_fits = Math.floor(remaining / coin); + return change.concat(Array(times_coin_fits).fill(coin)); + }, []); // change array starts out empty and gets filled itteratively. +}; diff --git a/lib/vending-machine.js b/lib/vending-machine.js new file mode 100644 index 0000000..7af9517 --- /dev/null +++ b/lib/vending-machine.js @@ -0,0 +1,78 @@ +const calculateChange = require('./change-calculator.js'); + +/** + * reduceCoinSupply removes the coins being given as change from the "supply" + * so that the vending machine does not attempt to give coins it does not have! + * @param {number} cashPaid the integer amount (in pennies) the person paid + * @param {array} [coinsAvail=COINS] the supply of coins to select change from. + * @param {array} changeGiven the list of coins returned as change. + * @returns {array} list of coins available in the vending machine + * @example reduceCoinSupply([200, 100, 50, 10, 1], [100, 50, 10]); // [200, 1] + */ +function reduceCoinSupply (coinsAvail, changeGiven) { + + let change = changeGiven.slice(); // clone changeGiven Array non-destructively + return coinsAvail.map(function (coin) { + + for (let i = 0; i < change.length; i++) { + if (change[i] === coin) { + change.splice(i, 1); + return; // return early! (removes the coin from the returned array!) + } + } + + return coin; // the "supply" of available coins + }).filter(Boolean); // avoid null values ¯\_(ツ)_/¯ JavaScript ... +} + +// GOLBAL Coins Available Array. +let COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +]; + +/** + * setCoinsAvail a "setter" for the COINS Array + * @param {Array} coinsAvail the list of available coins + */ +function setCoinsAvail (coinsAvail) { + COINS = coinsAvail; +} + +/** + * getCoinsAvail (a "getter") retrieves the COINS Array + */ +function getCoinsAvail () { + return COINS; +} + +/** + * sellProduct accepts three parameters (totalPayable, coinsPaid and coinsAvail) + * and calculates the change in "coins" that needs to be returned. + * @param {number} totalPayable the integer amount (in pennies) to be paid + * @param {array} coinsPaid the list of coins (in pennies) the person paid + * @param {array} [coinsAvail=COINS] the list of coins available to select from. + * @returns {array} list of coins we need to dispense to the person as change + * @example sellProduct(215, [200, 100]); // returns [50, 20, 10, 5] + */ +function sellProduct (totalPayable, coinsPaid, coinsAvail) { + const cashPaid = coinsPaid.reduce( function(sum, c) { return sum + c }, 0); + const change = calculateChange(totalPayable, cashPaid, coinsAvail); + // remove the coins we are about to return as change from the coinsAvail: + setCoinsAvail(reduceCoinSupply(coinsAvail, change)); + return change; +} + + +module.exports = { + reduceCoinSupply: reduceCoinSupply, + setCoinsAvail: setCoinsAvail, + getCoinsAvail: getCoinsAvail, + sellProduct: sellProduct +} diff --git a/package.json b/package.json index f50d5f2..40d5169 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "learn-tape", - "version": "2.0.3", + "version": "2.0.4", "repository": { "type": "git", "url": "https://github.com/dwyl/learn-tape.git" @@ -10,8 +10,9 @@ "devDependencies": { "browserify": "^16.2.2", "istanbul": "^0.4.5", - "jsdom": "^11.1.0", + "jsdom": "^12.2.0", "pre-commit": "^1.2.2", + "tap": "^12.0.1", "tap-spec": "^5.0.0", "tape": "^4.9.1", "testling": "^1.7.1" diff --git a/tap-advanced-testing.md b/tap-advanced-testing.md new file mode 100644 index 0000000..381097d --- /dev/null +++ b/tap-advanced-testing.md @@ -0,0 +1,952 @@ +
+ TAP +
+ +# _Why_ `Tap`? + +In _most_ situations **`Tape`** will be _exactly_ what you need +to write and run your Node.js/JavaScript tests.
+**`Tape`** is minimalist, fast and has flexibility when you need it. +_Occasionally_ however, the needs of the project +require a few extra features from the testing framework and +that is when we use **`Tap`**. + +If you find yourself (_your project_) needing to do "setup" +**`before`** running a test or "teardown" **`after`** the test is complete +(_e.g. resetting a mock or the state of a front-end app_), +***OR*** you have a few hundred/thousand tests which are taking a "long time" +to run (_e.g: more than 10 seconds_), +then you will benefit form switching/upgrading to **`Tap`**. + +The **`Tap`** repo has quite a good breakdown of the _reasons_ +why you should consider using **`Tap`**: +https://github.com/tapjs/node-tap#why-tap + +> _**`Tape`** has 5x more usage than **`Tap`** +according to NPM install stats.
+> This is due to a number of factors including "founder effect".
+But the fact that **`Tape`** has **fewer features** +has contributed to it's enduring/growing popularity.
+Ensure you have **used** **`Tape`** in at least one "real" project +**`before`** considering **`Tap`**, otherwise you risk complexity!_ + + + +# _What_? + +**`Tap`** is a testing framework that +has a few _useful_ extra features _without_ being "bloated". + ++ [x] `beforeEach` or `afterEach` for "resetting state" between tests. ++ [x] _parallel_ test execution: https://www.node-tap.org/parallel/ ++ [x] built-in code coverage: https://www.node-tap.org/coverage/ + +We will cover `beforeEach` by adding a real world "coin supply" +to our vending machine example below.
+ +> **`Tap`** has _many_ more "advanced" features, +if you are _curious_ see: https://www.node-tap.org/advanced
+But don't get distracted by the "bells and whistles", +focus on building your App/Project and if you find +that you are repeating yourself a lot in your tests, +open an issue describing the problem e.g: + +# _How_? + +Continuing our theme of a "_vending machine_", +let's consider the following _real world_ use case: +The Vending Machine "runs out" of coins and needs to be _re-filled_! + +We will add this functionality to the `calculateChange` function +and showcase a useful **`Tap`** feature: `beforeEach`
+Once those basics are covered, +we will dive into something _way_ more _ambitious_! + +## 1. Install `Tap` as a Dev Dependency + +Start by installing `Tap` ] +and saving it to your `package.json`: + +```sh +npm install tap --save-dev +``` + +## 2. Copy the **`Tape`** Test + +Copy the **`Tape`** test +so we can repurpose it for **`Tap`**: +```js +cp test/change-calculator.test.js test/change-tap.test.js +``` + +## 3. Change the First Line of the `test/change-tap.test.js` File + +Open the `test/change-tap.test.js` file and change the _first_ line +to require **`tap`** instead of **`tape`**. + +From: +```js +const test = require('tape'); +``` + +To: +```js +const test = require('tap').test; +``` + +## 4. Run the Test + +Run the **`Tap`** tests: + +```sh +node test/change-tap.test.js +``` + +The output is slightly different from **`Tape`**, +but the tests still pass: + +![tap-tests-pass](https://user-images.githubusercontent.com/194400/47609430-48d7ee00-da36-11e8-9f3c-f448f78311bb.png) + +## 5. New Requirement: Real World Coin "Supply" + +In the "real world" the coins in the machine will be given out +as change to the people buying products and may "run out". +Therefore having a _fixed_ Array of coins in the `calculateChange` function +is _artificially_ setting expectations that might not reflect _reality_. + +We need a way of (_optionally_) passing +the array of coins into the `calculateChange` function +but having a _default_ array of coins +so the _existing_ tests continue to pass. + + +> **Note**: for brevity, we are using the _functional_ solution +to `calculateChange` function.
+If this is unfamiliar to you, +please see: https://github.com/dwyl/learn-tdd#functional + +### 5.1 Add `coinsAvail` (_optional_) parameter to JSDOC + +Add a _new_ parameter to the JSDOC comment (_above the function_): + +Before: +```js +/** + * calculateChange accepts two parameters (totalPayable and cashPaid) + * and calculates the change in "coins" that needs to be returned. + * @param {number} totalPayable the integer amount (in pennies) to be paid + * @param {number} cashPaid the integer amount (in pennies) the person paid + * @returns {array} list of coins we need to dispense to the person as change + * @example calculateChange(215, 300); // returns [50, 20, 10, 5] + */ +``` + +After: +```js +/** + * calculateChange accepts three parameters (totalPayable, cashPaid, coinsAvail) + * and calculates the change in "coins" that needs to be returned. + * @param {number} totalPayable the integer amount (in pennies) to be paid + * @param {number} cashPaid the integer amount (in pennies) the person paid + * @param {array} [coinsAvail=COINS] the list of coins available to select from. + * @returns {array} list of coins we need to dispense to the person as change + * @example calculateChange(215, 300); // returns [50, 20, 10, 5] + */ +``` + +The important bit is: +```js + * @param {array} [coinsAvail=COINS] the list of coins available to select from. +``` + +`[coinsAvail=COINS]` means: ++ the argument will be called `coinsAvail` and ++ a default value of `COINS` (_the default Array of coins_) will be set +if the argument is undefined. ++ the `[ ]` (_square brackets_) around +the parameter definition indicates that it's _optional_. + +More detail on optional parameters in JSDOC comments,
+see: http://usejsdoc.org/tags-param.html#optional-parameters-and-default-values + +The _full_ file should now look like this: +[`lib/change-calculator.js`](https://github.com/dwyl/learn-tape/blob/d157a609ad8f2900588b1e118624b6e17d4c36c9/lib/change-calculator.js) + + +> At this point nothing in the `calculateChange` function or tests has changed, +so if you run the tests, +you should see _exactly_ the same output as above in step 4; +all tests still pass. + + +### 5.2 Add a _Test_ For the New `coinsAvail` Parameter + +Add the following test to your `test/change-tap.test.js` file: + +```js +test('Vending Machine has no £1 coins left! calculateChange(1337, 1500, [200, 50, 50, 50, 10, 5, 2, 1]) should equal [100, 50, 10, 2, 1 ]', function (t) { + const result = calculateChange(1337, 1500, [200, 50, 50, 50, 10, 5, 2, 1]); + const expected = [50, 50, 50, 10, 2, 1 ]; // £1.63 + t.deepEqual(result, expected, + 'calculateChange returns the correct change'); + t.end(); +}); +``` + +Run the **`Tap`** tests: + +```sh +node test/change-tap.test.js +``` + +You should see the test _fail_: + +![tap-test-fail](https://user-images.githubusercontent.com/194400/47610486-539e7d00-da4e-11e8-9989-2c39bc2403e7.png) + + + +### 5.3 Add `coinsAvail` (_optional_) Parameter to `calculateChange` Function + +Now that we've written the JSDOC for our function, +let's add the `coinsAvail` parameter to the `calculateChange` function: + +Before: +```js +module.exports = function calculateChange (totalPayable, cashPaid) { + ... +} +``` + +After: +```js +module.exports = function calculateChange (totalPayable, cashPaid, coinsAvail) { + ... +} +``` + +The tests will still not pass. Re-run them and see for yourself. + + +### 5.4 _Implement_ Default value `COINS` when `coinsAvail` is `undefined` + +Given that the `coinsAvail` parameter is _optional_ we need +a default value to work with in our function, +let's create a `coins` function-scoped variable +and use _conditional assignment_ to set `COINS` as the default value +of the Array if `coinsAvail` is undefined: + +Before: +```js +module.exports = function calculateChange (totalPayable, cashPaid, coinsAvail) { + return COINS.reduce(function (change, coin) { + // etc. + }, []); +} +``` + +After: +```js +module.exports = function calculateChange (totalPayable, cashPaid, coinsAvail) { + const coins = coinsAvail || COINS; // if coinsAvail param undefined use COINS + return coins.reduce(function (change, coin) { + // etc. + }, []); +} +``` + +This should be _enough_ code to make the test defined above in step 5.2 _pass_. +Save your changes to the `calculateChange` function and re-run the tests: + +```sh +node test/change-tap.test.js +``` + +You should see all the tests _pass_: + +![coinsAvail-test-passing](https://user-images.githubusercontent.com/194400/47610513-fbb44600-da4e-11e8-9593-683735cde5a6.png) + +That's it, you've satisfied the requirement of allowing a +`coinsAvail` ("Coins Available") array +to be passed into the `calculateChange` function. + +But hold on ... are we _really_ testing the _real-world_ scenario +where the coins are _removed_ from the `coinsAvail` ("supply") each time +the Vending Machine gives out change...? + +The answer is "No". +All our _previous_ tests assume an infinite supply of coins +and the _latest_ test simply passes in the array of `coinsAvail`, +we are not _removing_ the coins from the `coinsAvail` Array. + + +### 5.5 Reduce the "Supply" of Available Coins + +In the "_real world_" we need to reduce the coins in the vending machine +each time change is given. + +Imagine that the vending machine starts out +with a supply of **10** of _each_ coin: + +```js +let COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +]; +``` +This is a total of **80 coins**; 8 coin types x 10 of each type. + +Each time the machine returns coins we need to _remove_ them from the supply +otherwise we could end up giving the _incorrect_ change +because the machine may attempt to disburse coins which it does not _have_. + +Let's create a new module/function to implement this requirement. + +> _**Note**: for the purposes of this tutorial/example we are allocating +10 of each type of coin to the "supply".
+If we want represent the **actual** coins in circulation, +we would need to apportion coins according to their popularity,
see_: +https://en.wikipedia.org/wiki/Coins_of_the_pound_sterling#Coins_in_circulation +
+ +#### 5.5.1 Create the `lib/vending-machine.js` File + +Create the `new` file for the more "advanced" Vending Machine functionality: + +```sh +atom lib/vending-machine.js +``` + +#### 5.5.2 Write JSDOC for the `reduceCoinSupply` function + +Following "Documentation Driven Development" (DDD), +we write the JSDOC Documentation comment _first_ +and consider the function signature up-front. + +```js +/** + * reduceCoinSupply removes the coins being given as change from the "supply" + * so that the vending machine does not attempt to give coins it does not have! + * @param {number} cashPaid the integer amount (in pennies) the person paid + * @param {array} coinsAvail supply of coins to chose from when giving change. + * @param {array} changeGiven the list of coins returned as change. + * @returns {array} list of coins available in the vending machine + * @example reduceCoinSupply([200, 100, 50, 10, 1], [100, 50, 10]); // [200, 1] + */ +function reduceCoinSupply (coinsAvail, changeGiven) { + +} +``` + +By now the JSDOC syntax should be _familiar_ to you. +If not, see: https://github.com/dwyl/learn-jsdoc + +> Document-Driven Development as an engineering discipline +takes a bit of getting used to, +but it's _guaranteed_ to result in more maintainable code. +As a beginner, this may not be your primary focus, +but the more experienced you become as a engineer, +the more you will value maintainability; +writing maintainable code is a _super power_ everyone can achieve! + +#### 5.5.3 Write a _Test_ for the `reduceCoinSupply` function + +Create the file `test/vending-machine.test.js` +and add the following code to it: + +```js +const test = require('tap'); +const vendingMachine = require('../lib/vending-machine.js'); +const reduceCoinSupply = vendingMachine.reduceCoinSupply; + +tap.test('reduceCoinSupply([200, 100, 50, 10, 1], [100, 50, 10]); returns [200, 1]', function (t) { + const result = reduceCoinSupply([200, 100, 50, 10, 1], [100, 50, 10]); + const expected = [200, 1]; + t.deepEqual(result, expected, + 'reduceCoinSupply reduces the coinsAvail by the coins given as change'); + t.end(); +}); +``` + +Save the file and run the test: +```sh +node test/vending-machine.test.js +``` + +You should see it _fail_: + +![reduceCoinSupply-failing-test-not-a-function](https://user-images.githubusercontent.com/194400/47623074-9b7fdb80-db04-11e8-8b79-706d858b3b27.png) + +#### 5.5.4 Write _Just Enough_ Code to Make the Test Pass! + +In the `lib/vending-machine.js` file, +write _just_ enough code in the `reduceCoinSupply` function body +to make the test pass. + +Remember to _export_ the function at the end of the file: + +```js +module.exports = { + reduceCoinSupply: reduceCoinSupply +} +``` + +Try to solve this challenge yourself (_or in pairs/teams_) +_before_ looking at the "solution": +[`lib/vending-machine.js > reduceCoinSupply`](https://github.com/dwyl/learn-tape/blob/4237175283ff36777fb7631aa52cbbefbf578853/lib/vending-machine.js#L10) + +When it passes you should see: + +![reduceCoinSupply-test-passing](https://user-images.githubusercontent.com/194400/47623473-e0f2d780-db09-11e8-9dbb-48493ee0352e.png) + + +#### 5.5.5 Write _Another_ Test Example! + +To "_exercise_" the `reduceCoinSupply` function, +let's add another test example.
+Add the following test to the `test/vending-machine.test.js` file: + +```js +tap.test('reduceCoinSupply remove more coins!', function (t) { + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + const remove = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, + 5, 5, 5, 5, + 2, 2, 2, + 1, 1, + ]; + const expected = [ + 200, + 100, 100, + 50, 50, 50, + 20, 20, 20, 20, + 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1 + ]; + + const result = reduceCoinSupply(COINS, remove); + t.deepEqual(result, expected, + 'reduceCoinSupply removes coins from supply of coinsAvail'); + t.end(); +}); +``` + +You should not need to make any changes to your `reduceCoinSupply` function. +Simply saving the test file and re-running the Tap tests: + +```sh +node test/vending-machine.test.js +``` + +You should see the _both_ tests pass: + +![remove-more-coins-passing](https://user-images.githubusercontent.com/194400/47623857-f8cc5a80-db0d-11e8-82da-e828a00e7ca6.png) + + +So far so good, we have a way of reducing the coins available +but there is still an +["elephant in the room"](https://en.wikipedia.org/wiki/Elephant_in_the_room) ... +how does the Vending Machine **keep track** +of the **coins** it has _**available**_? +i.e the "**state**" of the machine. + +### 5.6 How Does the Vending Machine Maintain "State"? + +The vending machine needs to "_know_" how many coins it still has, +to avoid trying to give coins it does not have available. + +> There are a _number_ of ways of doing "state management". +Our favourite is the Elm Architecture. +We wrote a _dedicated_ tutorial for it; see: +https://github.com/dwyl/learn-elm-architecture-in-javascript
+However to illustrate a _less_ sophisticated state management +we are using a _global_ `COINS` Array. + +#### 5.6.1 Create a Test for the Vending Machine `COINS` "Initial State" + +In your `test/vending-machine.test.js` file add the following code: + +```js +tap.test('Check Initial Supply of Coins in Vending Machine', function (t) { + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + t.deepEqual(vendingMachine.getCoinsAvail(), COINS, + 'vendingMachine.getCoinsAvail() gets COINS available in Vending Machine'); + vendingMachine.setCoinsAvail([1,2,3]); + t.deepEqual(vendingMachine.getCoinsAvail(), [1,2,3], + 'vendingMachine.setCoinsAvail allows us to set the COINS available'); + t.end(); +}); +``` + +If you run the tests, + +```sh +node test/vending-machine.test.js +``` + +you will see the last one fail: +![coin-supply-test-fail](https://user-images.githubusercontent.com/194400/47670542-5cee2d80-dba5-11e8-820b-811de276d8dd.png) + +#### 5.6.2 Write the Code to Make the Test _Pass_ + +In `lib/vending-machine.js` file, +after the `reduceCoinSupply` function definition, +add the following variable declaration and pair of functions: + +```js +// GOLBAL Coins Available Array. 10 of each type of coin. +let COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +]; + +/** + * setCoinsAvail a "setter" for the COINS Array + * @param {Array} coinsAvail the list of available coins + */ +function setCoinsAvail (coinsAvail) { + COINS = coinsAvail; +} + +/** + * getCoinsAvail a "getter" for the COINS Array + */ +function getCoinsAvail () { + return COINS; +} +``` + +We will _use_ the `COINS` array +and both the "getter" and "setter" +in the next step. +For now, simply _export_ "getter" and "setter" so the test will pass: + +```js +module.exports = { + reduceCoinSupply: reduceCoinSupply, + setCoinsAvail: setCoinsAvail, + getCoinsAvail: getCoinsAvail, +} +``` + +Re-run the test and watch it _pass_: + +![coins-test-passing](https://user-images.githubusercontent.com/194400/47670720-d2f29480-dba5-11e8-8187-48b1b3fa7017.png) + + +### 5.7 Vending Machine > `sellProduct` Function + +We already have a `calculateChange` function +which calculates the change for a given amount of money received +and price of product being purchased, +and we have the `reduceCoinSupply` function which removes coins +from the "supply" the vending machine has. +These can be considered "internal" functions. + +Now _all_ we have to do is _combine_ these two functions into a +"public interface" which handles _both_ calculating change +and disbursing the coins from the supply. + +#### 5.7.1 Write JSDOC Comment for `sellProduct` Function + +Add the following JSDOC comment and function signature to +the `lib/vending-machine.js` file: + +```js +/** + * sellProduct accepts three parameters (totalPayable, coinsPaid and coinsAvail) + * and calculates the change in "coins" that needs to be returned. + * @param {number} totalPayable the integer amount (in pennies) to be paid + * @param {array} coinsPaid the list of coins (in pennies) the person paid + * @param {array} [coinsAvail=COINS] the list of coins available to select from. + * @returns {array} list of coins we need to dispense to the person as change + * @example sellProduct(215, [200, 100]); // returns [50, 20, 10, 5] + */ +function sellProduct (totalPayable, coinsPaid, coinsAvail) { + +} +``` + +Things to note here: +the JSDOC and function signature +are _similar_ to the `calculateChange` function +the key distinction is the second parameter: `coinsPaid`. +The vending machine receives a list of _coins_ when the person makes a purchase. + + +#### 5.7.2 Craft a _Test_ for the `sellProduct` Function + +In your `test/vending-machine.test.js` file add the following code: + +```js +tap.test('sellProduct(215, [200, 100], COINS) returns [50, 20, 10, 5]', function (t) { + const COINS = vendingMachine.getCoinsAvail(); + const coinsPaid = [200, 100]; + const result = vendingMachine.sellProduct(215, coinsPaid, COINS); + const expected = [50, 20, 10, 5]; + t.deepEqual(result, expected, + 'sellProduct returns the correct coins as change'); + + // check that the supply of COINS Available in the vendingMachine was reduced: + t.deepEqual(vendingMachine.getCoinsAvail(), reduceCoinSupply(COINS, result), + 'running the sellProduct function reduces the COINS the vending machine'); + t.end(); +}); +``` + +> _**Note**: you will have noticed both from the JSDOC and +the test invocation that the `sellProduct` function returns **one** +array; the list of coins to give the customer as change. +JavaScript does not have a +["tuple"](https://elixir-lang.org/getting-started/basic-types.html) +primitive +(which would allow a function to return multiple values), +so we can either return an `Object` in the `sellProduct` function +or `return` **just** the Array of coins +to be given to the customer as change. +The second assertion in the test shows that the COINS array in +vendingMachine module is being "mutated" by the `sellProduct` function. +This is an **undesirable** "**side effect**" but +this illustrates something you are likely to see in the "wild". +If you feel "uncomfortable" with this "impure" style, and you should, +consider learning a functional language like Elm, Elixir or Haskell_ +_JavaScript "works", but it's **ridiculously easy** +to **inadvertently introduce bugs** and "unsafety". +which is why [sanctuary](https://github.com/sanctuary-js/sanctuary) +exists._
+ + +If you run the tests: + +```sh +node test/vending-machine.test.js +``` + +you will see the last one _fail_: + +![sellProduct-test-fail](https://user-images.githubusercontent.com/194400/47678596-1bb44880-dbba-11e8-8927-892149a201bd.png) + + +#### 5.7.3 _Implement_ the `sellProduct` Function to Pass the Test + +The `sellProduct` function will _invoke_ the `calculateChange` function, +so the _first_ thing we need to do is import it. + +At the top of the `lib/vending-machine.js` file, +add the following line: +```js +const calculateChange = require('./change-calculator.js'); +``` + +Now we can implement `sellProduct` which should only a few lines +invoking other functions. +Again, try implementing it yourself (_or in pairs/teams_), +before looking at the +[`vendingMachine.sellProduct` solution]() + +Don't forget to **`export`** the `sellProduct` function. + + +#### 5.7.4 What is the State? + +During the execution of our last test for the `sellProduct` function, +the `COINS` array in the Vending Machine has been altered. + +Let's write another test to illustrate this. + +In your `test/vending-machine.test.js` file add the following code: + +```js +tap.test('Check COINS Available Supply in Vending Machine', function (t) { + const coinsAvail = vendingMachine.getCoinsAvail() + t.equal(coinsAvail.length, 76, + 'vendingMachine.getCoinsAvail() shows COINS in Vending Machine is ' + + coinsAvail.length); + t.end(); +}); +``` + +If you followed all the previous steps the test will pass, +meaning that the `COINS` array in the `vendingMachine` +was _modified_ by the _previous_ (`sellProduct`) test. + +![COINS-available-76](https://user-images.githubusercontent.com/194400/47731483-1c9db680-dc5c-11e8-9e22-5fc295ab36d5.png) + +The _initial_ state for the `COINS` array +that we defined in step **5.6.2** (_above_) +was **80 Coins**, +but after executing the `sellProduct` test it's **76 coins**. + +It both _simulates_ the "***real world***" and creates a testing "_headache_"; +modifying the `COINS` array (_the vending machine's coins available "state"_) +is _desirable_ because in the _real world_ each time an item is sold +the state of `COINS` _should_ be updated. +But it means we need to "_reset_" the `COINS` array between _tests_ +otherwise we will end up writing _coupled_ ("_co-dependent_") tests. + + +### 5.8 _Reset_ `COINS` State in Vending Machine Between Tests? + +Given that the `COINS` state in the vending machine is being +modified by the `sellProduct` function, +and the fact that we don't _want_ tests to be co-dependent, +we _either_ need to "reset" the state of `COINS` _inside_ each test, +***or*** we need to reset the state of `COINS` _before_ each test. + +Here is the difference: + +#### 5.8.1 Reset `COINS` State at the Top of _Each_ Test + +```js +tap.test('Check COINS Available Supply in Vending Machine', function (t) { + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + vendingMachine.setCoinsAvail(COINS); + const coinsAvail = vendingMachine.getCoinsAvail(); + t.equal(coinsAvail.length, 80, + 'vendingMachine.getCoinsAvail() shows COINS in Vending Machine is ' + + coinsAvail.length); + t.end(); +}); + +tap.test('sellProduct(1337, [1000, 500], coinsAvail) >> [100, 50, 10, 2, 1]', function (t) { + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + vendingMachine.setCoinsAvail(COINS); + let coinsAvail = vendingMachine.getCoinsAvail(); + const expected = [100, 50, 10, 2, 1]; + const result = vendingMachine.sellProduct(1337, [1000, 500], coinsAvail) + t.equal(result, expected, + 'sellProduct(1337, [1000, 500], coinsAvail) is ' + result); + + coinsAvail = vendingMachine.getCoinsAvail(); + t.equal(coinsAvail.length, 75, // 80 minus the coins dispensed (5) + 'vendingMachine.getCoinsAvail() shows COINS in Vending Machine is ' + + coinsAvail.length); + t.end(); +}); +``` + +As you can see these two tests share the same "setup" or "reset state" code: + +```js +const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +]; +vendingMachine.setCoinsAvail(COINS); +const coinsAvail = vendingMachine.getCoinsAvail(); +``` + +We can _easily_ remove this identical duplication of code +by using one of **`Tap`**'s built-in testing helper functions: `beforeEach`! + +#### 5.8.1 Reset `COINS` State `beforeEach` Test + +We can re-write the two preceding tests +and eliminate the duplication, +by using the **`Tap`** `beforeEach` function +to reset the test _before_ each test: + + + +```js +tap.beforeEach(function (done) { // reset state of COINS before each test: + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + vendingMachine.setCoinsAvail(COINS); + done() +}); + +tap.test('Check COINS Available Supply in Vending Machine', function (t) { + const coinsAvail = vendingMachine.getCoinsAvail(); + t.equal(coinsAvail.length, 80, + 'vendingMachine.getCoinsAvail() shows COINS in Vending Machine is ' + + coinsAvail.length); + t.end(); +}); + +tap.test('sellProduct(1337, [1000, 500], coinsAvail) >> [100, 50, 10, 2, 1]', function (t) { + let coinsAvail = vendingMachine.getCoinsAvail(); + const expected = [100, 50, 10, 2, 1]; + const result = vendingMachine.sellProduct(1337, [1000, 500], coinsAvail) + t.deepEqual(result, expected, + 'sellProduct(1337, [1000, 500], coinsAvail) is ' + result); + + coinsAvail = vendingMachine.getCoinsAvail(); + t.equal(coinsAvail.length, 75, // 80 minus the coins dispensed (5) + 'vendingMachine.getCoinsAvail() shows COINS in Vending Machine is ' + + coinsAvail.length); + t.end(); +}); +``` + +Both of these approaches will give the same _result_: + +![before-each-test-passing](https://user-images.githubusercontent.com/194400/47737541-0ea26280-dc69-11e8-96b2-f41257fb802b.png) + +But the `beforeEach` approach has an _immediate_ benefit in terms of +reduction of duplicate code in tests +which is less to read (_reduces cognitive load_) +and easier to maintain (_if the "reset state" code needs to be changed, +it's changed in one place_). + + +# _Now_ What? + +If you found this _taster_ on testing with **`Tap`** +helpful, consider reading our more _comprehensive_ Todo List example: +https://github.com/dwyl/todomvc-vanilla-javascript-elm-architecture-example . + + + + +


+ +## Analogy: _Single_ Speed vs. _Geared_ Bicycle + + +To the _untrained_ observer, + + +
+ + single speed bicycle - perfect for short trips on fairly flat ground + +
+
+ +**`Tape`** is like a **single speed** bicycle; +lightweight, fewer "moving parts", less to learn and _fast_!
+_Perfect_ for **_short_ trips** on _relatively_ **_flat_ terrain**. +_Most_ journeys in cities fit this description. +_Most_ of the time you won't _need_ anything more than this +for commuting from home to work, going to the shops, etc. + + +
+ + geared bicycle bicycle - for longer distances and hilly terrain + +
+
+ +**`Tap`** is the bicycle _with **gears**_ +that allows you to tackle different _terrain_. +"Gears" in the context of writing unit/end-to-end tests +is having a `t.spawn` (_run tests in a separate process_), +or running tests in parallel so they finish faster; +i.e. reducing the _effort_ required to cover the same distance +and in some cases speeding up the execution of your test suite. + + + +> **Note**: This analogy falls down if your commuting distance is far; +you need a geared bicycle for the long stretches! +Also, if you _never_ ride a bicycle - for whatever reason - +and don't appreciate the difference between +single speed and geared bikes this analogy might feel less relevant ... +in which case we recommend a bit of background reading: +https://bicycles.stackexchange.com/questions/1983/why-ride-a-single-speed-bike + + +
+ +# _Why_ NOT Use `Tap` _Everywhere_? + +One of the _benefits_ of Free/Open Source software +is that there is near-zero +["switching cost"](https://en.wikipedia.org/wiki/Switching_barriers). +Since we aren't paying for the code, +the only "cost" to adopting a new tool is **_learning_ time**. + +Given that **`Tap`** is a "drop-in replacement" for **`Tape`** +in _most_ cases, the switching cost is just **`npm install tap --save-dev`** +followed by a find-and-replace across the files in your project from: +**`require('tape')`** to **`require('tap').test`**. + +Over the last 5 years @dwyl we have tested **200+** projects using **`Tape`** +(_both Open Source and Client projects_). +We have no reason for "_complaint_" or criticism of **`Tape`**, +we will _not_ be spending time switching +our _existing_ projects from **`Tape`** to **`Tap`** +because in most cases, there is **no _need_**; +[YAGNI!](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it) + +Where we _are_ using **`Tap`** is for the _massive_ projects +where we either need to do a lot of state re-setting e.g: **`t.afterEach`** +or simply where test runs take longer than 10 seconds +(_because we have lots of end-to-end tests_) +and running them in ***parallel*** significantly reduces waiting time. + +For an _extended_ practical example of where writing tests with **`Tap`** +_instead_ of **`Tape`** was worth the switch, +see: https://github.com/dwyl/todomvc-vanilla-javascript-elm-architecture-example . diff --git a/test/change-tap.test.js b/test/change-tap.test.js new file mode 100644 index 0000000..7a58df4 --- /dev/null +++ b/test/change-tap.test.js @@ -0,0 +1,38 @@ +const test = require('tap').test; +const calculateChange = require('../lib/change-calculator.js'); // require the calculator module + +test('calculateChange(215, 300) should return [50, 20, 10, 5]', function(t) { + const result = calculateChange(215, 300); // expect an array containing [50,20,10,5] + const expected = [50, 20, 10, 5]; + t.deepEqual(result, expected); + t.end(); +}); + +test('calculateChange(486, 600) should equal [100, 10, 2, 2]', function(t) { + const result = calculateChange(486, 600); + const expected = [100, 10, 2, 2]; + t.deepEqual(result, expected); + t.end(); +}); + +test('calculateChange(12, 400) should return [200, 100, 50, 20, 10, 5, 2, 1]', function(t) { + const result = calculateChange(12, 400); + const expected = [200, 100, 50, 20, 10, 5, 2, 1]; + t.deepEqual(result, expected); + t.end(); +}); + +test('calculateChange(1487,10000) should equal [5000, 2000, 1000, 500, 10, 2, 1 ]', function(t) { + const result = calculateChange(1487,10000); + const expected = [5000, 2000, 1000, 500, 10, 2, 1 ]; + t.deepEqual(result, expected); + t.end(); +}); + +test('Vending Machine has no £1 coins left! calculateChange(1337, 1500, [200, 50, 50, 50, 10, 5, 2, 1]) should equal [100, 50, 10, 2, 1 ]', +function (t) { + const result = calculateChange(1337, 1500, [200, 50, 50, 50, 10, 5, 2, 1]); + const expected = [50, 50, 50, 10, 2, 1 ]; // £1.63 + t.deepEqual(result, expected); + t.end(); +}); diff --git a/test/vending-machine.test.js b/test/vending-machine.test.js new file mode 100644 index 0000000..30139c8 --- /dev/null +++ b/test/vending-machine.test.js @@ -0,0 +1,129 @@ +const tap = require('tap'); +const vendingMachine = require('../lib/vending-machine.js'); +const reduceCoinSupply = vendingMachine.reduceCoinSupply; + +tap.test('reduceCoinSupply([200, 100, 50, 10, 1], [100, 50, 10]); returns [200, 1]', function (t) { + const result = reduceCoinSupply([200, 100, 50, 10, 1], [100, 50, 10]); + const expected = [200, 1]; + t.deepEqual(result, expected, + 'reduceCoinSupply reduces the coinsAvail by the coins given as change'); + t.end(); +}); + +tap.test('reduceCoinSupply remove more coins!', function (t) { + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + const remove = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, + 5, 5, 5, 5, + 2, 2, 2, + 1, 1, + ]; + const expected = [ + 200, + 100, 100, + 50, 50, 50, + 20, 20, 20, 20, + 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1 + ]; + + const result = reduceCoinSupply(COINS, remove); + t.deepEqual(result, expected, + 'reduceCoinSupply removes coins from supply of coinsAvail'); + t.end(); +}); + +tap.test('Check Initial Supply of Coins in Vending Machine', function (t) { + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + t.deepEqual(vendingMachine.getCoinsAvail(), COINS, + 'vendingMachine.getCoinsAvail() gets COINS available in Vending Machine'); + vendingMachine.setCoinsAvail([1,2,3]); + t.deepEqual(vendingMachine.getCoinsAvail(), [1,2,3], + 'vendingMachine.setCoinsAvail allows us to set the COINS available'); + t.end(); +}); + +tap.test('sellProduct(215, [200, 100], COINS) returns [50, 20, 10, 5]', function (t) { + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + vendingMachine.setCoinsAvail(COINS); + const coinsPaid = [200, 100]; + const result = vendingMachine.sellProduct(215, coinsPaid, + vendingMachine.getCoinsAvail()); + const expected = [50, 20, 10, 5]; + t.deepEqual(result, expected, + 'sellProduct returns the correct coins as change'); + // check that the supply of COINS Available in the vendingMachine was reduced: + t.deepEqual(vendingMachine.getCoinsAvail(), reduceCoinSupply(COINS, result), + 'running the sellProduct function reduces the COINS the vending machine'); + t.end(); +}); + +tap.beforeEach(function (done) { // reset state of COINS before each test: + const COINS = [ + 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]; + vendingMachine.setCoinsAvail(COINS); + done() +}); + +tap.test('Check COINS Available Supply in Vending Machine', function (t) { + const coinsAvail = vendingMachine.getCoinsAvail(); + t.equal(coinsAvail.length, 80, + 'vendingMachine.getCoinsAvail() shows COINS in Vending Machine is ' + + coinsAvail.length); + t.end(); +}); + +tap.test('sellProduct(1337, [1000, 500], coinsAvail) >> [100, 50, 10, 2, 1]', function (t) { + let coinsAvail = vendingMachine.getCoinsAvail(); + const expected = [100, 50, 10, 2, 1]; + const result = vendingMachine.sellProduct(1337, [1000, 500], coinsAvail) + t.deepEqual(result, expected, + 'sellProduct(1337, [1000, 500], coinsAvail) is ' + result); + + coinsAvail = vendingMachine.getCoinsAvail(); + t.equal(coinsAvail.length, 75, // 80 minus the coins dispensed (5) + 'vendingMachine.getCoinsAvail() shows COINS in Vending Machine is ' + + coinsAvail.length); + t.end(); +});