Skip to content

Latest commit

 

History

History
952 lines (737 loc) · 31.6 KB

tap-advanced-testing.md

File metadata and controls

952 lines (737 loc) · 31.6 KB
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".

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:

npm install tap -D

2. Copy the Tape Test

Copy the Tape test so we can repurpose it for Tap:

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:

const test = require('tape');

To:

const test = require('tap').test;

4. Run the Test

Run the Tap tests:

node test/change-tap.test.js

The output is slightly different from Tape, but the tests still pass:

tap-tests-pass

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:

/**
 * 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:

/**
 * 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:

 * @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

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:

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:

node test/change-tap.test.js

You should see the test fail:

tap-test-fail

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:

module.exports = function calculateChange (totalPayable, cashPaid) {
  ...
}

After:

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:

module.exports = function calculateChange (totalPayable, cashPaid, coinsAvail) {
  return COINS.reduce(function (change, coin) {
    // etc.
  }, []);
}

After:

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:

node test/change-tap.test.js

You should see all the tests pass:

coinsAvail-test-passing

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:

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:

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.

/**
 * 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:

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:

node test/vending-machine.test.js

You should see it fail:

reduceCoinSupply-failing-test-not-a-function

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:

module.exports = {
  reduceCoinSupply: reduceCoinSupply
}

Try to solve this challenge yourself (or in pairs/teams) before looking at the "solution": lib/vending-machine.js > reduceCoinSupply

When it passes you should see:

reduceCoinSupply-test-passing

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:

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:

node test/vending-machine.test.js

You should see the both tests pass:

remove-more-coins-passing

So far so good, we have a way of reducing the coins available but there is still an "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:

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,

node test/vending-machine.test.js

you will see the last one fail: coin-supply-test-fail

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:

// 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:

module.exports = {
  reduceCoinSupply: reduceCoinSupply,
  setCoinsAvail: setCoinsAvail,
  getCoinsAvail: getCoinsAvail,
}

Re-run the test and watch it pass:

coins-test-passing

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:

/**
 * 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:

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" 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 exists.

If you run the tests:

node test/vending-machine.test.js

you will see the last one fail:

sellProduct-test-fail

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:

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:

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

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

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:

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:

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

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,


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.


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". 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 -D 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!

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 .