ES6 TDD Warm-up


We're going to practice JavaScript. This guide will cover ES6 syntax, testing with Mocha and Chai, and basic iterative and recursive arithmetic functions developed using Test Driven Development (TDD).

Clone this repository to get started:

git clone https://bitbucket.org/eloquently/unit_01_es6_warmup.git

Next, install the packages needed to run this code using the following command:

npm install

Node Package Manager (npm) is the JavaScript analog to Ruby's Gem and Bundler.

In this directory structure, our code goes in the src directory and the tests go in the test directory.

Arithmetic

We are going to learn JavaScript by implementing some basic arithmetic functions. Don't worry, this is more exciting and challenging than it sounds!

increment()

Our first function will be called increment(). It will take one parameter and return the sum of the parameter and 1.

Write the function in index.js:

src/index.js
function increment(x) {
    return x + 1;
}

Let's also output the results. In JavaScript, we output results with the function console.log:

src/index.js
// ...

console.log(increment(1));

To run the JavaScript file, we will use a program called Node. To run this program, type the following command in your terminal:

node src/index.js

You should see the number 2 printed.

We're going to write a test for this function. We'll write the test in another program. In order to import the function in the other program, we'll have to export it from index.js. To do this, simply add the word export before the function declaration:

src/index.js
export function increment(x) {
    return x + 1;
}

Now try to run the program with Node:

node src/index.js

We see an error. This is because the export syntax is part of ES6, and node cannot run ES6. To get around this, we are going to use tools called Webpack and Babel to transform our ES6 JavaScript into a form of JavaScript that Node understands. We've already configured this application so that you just need to run this command to compile your index.js file and all the packages and other files it depends on into a single file called bundle.js:

npm run webpack

Now run this command in the terminal to run your program:

node build/bundle.js

The program should run as normal again.

If you don't want to type npm run webpack each time you change your files, open a new terminal and run npm run webpack:watch. Make sure your new terminal is in the correct directory. This process will watch your files and run webpack each time they are saved.

Open test/index_spec.js and add the following line:

test/index_spec.js
import { expect } from 'chai';

import { increment } from '../src/index.js';

Now we have access to the increment function from this test file. Every time you want to access a function or class from another file, you will need to export it from the file it is defined in and import it in the file you want to use it in.

We will use describe() and it() to keep our tests organized. Each nested describe() will show up indented one level in the tests. The first argument passed to describe is the name of the thing we are testing. Nothing magical happens with the name string, so feel free to type whatever you want. The second argument is a function that contains more describe blocks or tests.

Each it block corresponds to one test. Each test should check for one thing and should be mostly independent on other tests. Like the describe blocks, the it blocks take a string as the first argument. You should choose a string that reads like a grammatical sentence. The second argument to an it block is a function that contains the actual code for our test.

Here's how we'll set up the first test:

test/index_spec.js
// ...

describe('index.js', () => {
    describe('increment()', () => {
        it('adds 1 to the parameter', () => {
            expect(increment(1)).to.eq(2);
        });
    });
});

We can read this test like this:

  • These tests are for index.js
    • First, we'll test the increment function
      • It adds one to the parameter
        • Specifically, we expect increment(1) to equal 2

All test files will have this basic structure. There is nothing magical about any of the strings in the above test file. You can write any thing you want and the tests will still behave the same way -- they only affect how the tests are output to the console.

After saving the test file, we run the test by typing the following command in the terminal:

npm run test

You should see that the test passes.

Similar to webpack, we've provided you with a "watch" version of the run test command that will run your tests each time a file is saved:

npm run test:watch

Let's write another test for this function. We want to ensure that it works when the parameter is negative.

test/index_spec.js
// ...

describe('index.js', () => {
    describe('increment()', () => {
        it('adds 1 to the parameter', () => {
            expect(increment(1)).to.eq(2);
        });

        it('works with negative numbers', () => {
            expect(increment(-5)).to.eq(/*Insert number here*/);
        });
    });
});

Now that we've completed the first function, let's stage our code and then commit:

git add -A
git commit -m "increment function"

Now we want to write another function: add. This function will take two numbers and return their sum. For the sake of the exercise, it is not allowed to use the + operator -- only the increment function we've already written and loops and if statements. First, let's write a test for the function. You'll also need to import the add function at the top of the test file.

test/index_spec.js
import { expect } from 'chai';

import { increment, add } from '../src/index.js';

describe('index.js', () => {
    describe('increment()', () => {
        // ...
    });

    describe('add()', () => {
        it('adds two parameters', () => {
            expect(add(4, 6)).to.eq(10);
        });
    });
});

Now, let's write a function that will get the test to pass:

src/index.js
// ...

export function add(x, y) {
    let solution = x;
    for(let i = 0; i < y; i++) {
        solution = increment(solution);
    }
    return solution;
}

What is this function doing? First it creates a variable called solution and sets it equal to x, the first parameter. Then it starts a loop. for loops are very common in JavaScript. This loop takes three lines of code. It runs the first statement (let i = 0) before starting the loop. This statement creates a variable i and sets it initial value to 0. Then, at the start of each iteration of the loop (including the first one), it checks to see if i is less than y. If it is, it runs the body of the loop. After running through the body of the loop, it runs the third statement (i++). This statement adds one to i and saves that value. i++ is equivalent to i += 1, which is equivalent to i = i + 1 (we could also write it as increment(i)). It will continue looping until the condition in the second statement is false.

In English, a terse description of this function would be "increment x y times". A more detailed description would be:

  • Create a variable solution, and set it to x
  • y times, do the following:
    • increment solution and store the value in solution
  • return solution

Try replacing x and y with actual numbers, and see if the descriptions make sense to you.

Let's add another test for add():

test/index_spec.js
//...

describe('index.js', () => {
    // ...

    describe('add()', () => {
        // ...

        it('works if second parameter is 0', () => {
            expect(add(4, 0)).to.eq(4);
        });
    });
});

This test should pass with the method as it is currently written.

Now let's add another test:

test/index_spec.js
//...

describe('index.js', () => {
    // ...

    describe('add()', () => {
        // ...

        it('works if first parameter is negative', () => {
            expect(add(-2, 4)).to.eq(2);
        });
    });
});

This test should also pass. Now another:

test/index_spec.js
//...

describe('index.js', () => {
    // ...

    describe('add()', () => {
        // ...

        it('works if second parameter is negative', () => {
            expect(add(4, -3)).to.eq(1);
        });
    });
});

Uh oh! This test failed. It says the actual value we got was 4 but the expected value was 1. Let's fix this. We'll fix it by creating a decrement function that looks like the increment function:

Let's first write some tests so that we understand what decrement() does:

test/index_spec.js
// ...

import { increment, add, decrement } from '../src/index.js';

describe('index.js', () => {
    describe('increment()', () => {
        // ...
    });

    describe('decrement()', () => {
        it('subtracts 1 from the parameter', () => {
            expect(decrement(3)).to.eq(/* Insert number here */);
        });

        it('works with negative numbers', () => {
            /* Write this test yourself! */
        });
    });

    describe('add()', () => {
        // ...
    });
});

Now add decrement() to index.js:

src/index.js
export function increment(x) {
    // ...
}

export function decrement(x) {
    /* Write this function */
}

export function add(x, y) {
    // ...
}

Now we're ready to fix our add function so it will work when y is negative:

src/index.js
// ...

export function add(x, y) {
    let solution = x;
    if(y >= 0) {
        for(let i = 0; i < y; i++) {
            solution = increment(solution);
        }
    } else {
        for(let i = y; i < 0; i++) {
            /* Fill in the blank */
        }
    }
    return solution;
}

After the tests pass, commit your code!

Now it's time for you to write a multiply function. Your multiply function should only use the add function we wrote before and loops and if statements. You are also allowed to make a positive number negative (such as -x). Like add() it should work when either of the parameters are negative or when they are 0. You should write the tests before writing the function:

test/index_spec.js
import { expect } from 'chai';

import { increment, add, decrement, multiply } from '../src/index.js';

describe('index.js', () => {
    // ...

    describe('multiply()', () => {
        /* Write tests for multiply here. */
        /* You should have tests for when the parameters are negative and 0 */
        /* Feel free to borrow code from the tests we wrote for add() */
    });
});

If you are having trouble with the function, think about how you would solve 5 * 2 if you only knew how to add. How would you solve 5 * 3? How about 5 * 4?

Once you have a guess, try writing the function. You don't need all the tests to pass right away -- it's okay to solve them one at a time.

src/index.js
// ...

export function multiply(x, y) {
    /* Multiply code goes here */
}

After you get all the tests to pass, commit your code!

Finally, we want to practice recursion. We could write the add() as a recursive function instead of an interative one (iterative usually means that it uses a loop). A recursive function is one that calls itself. This is what the add function would look like if it was written recursively:

src/index.js
export function add(x, y) {
    if(y == 0) {
        return x;
    }
    else if(y > 0) {
        return increment(add(x, decrement(y)));
    }
    else {
        /* What goes here? */
    }
}

All the tests for add() should pass.

Now refactor your multiply function so that it is recursive rather than iterative.

ES6 Features

Short-hand Function Declaration

What are those () => { ... } symbols in the describe and it functions in the tests?

That is a short-hand way to declare a function in ES6. describe() and it() are both functions that take functions as their second parameters. We are giving them functions that take no parameters as their second argument. This is the same way that describe and it blocks work in Ruby's Rspec.

In ES6, it's common to use the short-hand whenever passing a function as as parameter. Array functions, such as map and reduce are commonly used functions that take a function as a parameter.

Let's practice this by writing a function that adds 1 to each element in an array. First the test:

test/index_spec.js
// ...

import { increment, add, decrement,
         multiply, incrementEach } from '../src/index.js';

describe('index.js', () => {
    // ...

    describe('incrementEach()', () => {
        it('increases each element', () => {
            expect(incrementEach([1, 2, 3])).to.eql([2, 3, 4]);
        });
    });
});

In this test, we use eql() instead of eq(). When comparing two arrays with eq() it will check to see if their references are equal (i.e. they point to same place in memory). When comparing two arrays with eql(), Chai will check that their values are the same.

Now for the short-hand function call:

src/index.js
// ...

export function incrementEach(array) {
    return array.map((element) => element + 1);
}

In this case, our function is only one line and it returns the value calculated. This means we can omit the {}s and the return.

Now you try implementing a decrementEach function. First the test:

test/index_spec.js
// ...

import { increment, add, decrement,
         multiply, incrementEach, decrementEach } from '../src/index.js';

describe('index.js', () => {
    // ...

    describe('decrementEach()', () => {
        it('decreases each element', () => {
            expect(decrementEach([1, 2, 3])).to.eql(/* Fill in the blank */);
        });
    });
});

Now the method:

src/index.js
// ...

export function decrementEach(array) {
    /* Fill in the blank */
}

Multiple Assignment

If we wanted to assign multiple variables to the results of a function that returns an array, ES6 allows us to do so very easily. Here's what it looks like:

test/index_spec.js
// ...

describe('index.js', () => {
    // ...

    describe('incrementEach()', () => {
        // ...

        it('multiple assignment example', () => {
            const [a, b] = incrementEach([10, 20]);
            expect(a).to.eq(11);
            expect(b).to.eq(21);
        });
    });

    // ...
});

const vs. let

In ES6, we have to declare variables when they are set for the first time (or before). We do this by using let or const. If an object will change references (or a primitive will change values), use let. Otherwise use const. Using const with objects (such as arrays) can be confusing because a const object is allowed to mutuate. const just means that the variable can't start pointing to a new object.

Here are some examples:

const a = 1;
a += 1; // NOT ALLOWED

let b = 1;
b += 1; // ALLOWED

const arr = [1, 2, 3];
arr[0] = 10; // ALLOWED
arr.push(4); // ALLOWED
arr = [4, 5, 6]; // NOT ALLOWED
arr = arr.map((el) => el + 1); // NOT ALLOWED

const obj = { "a": 1, "b": 2 }
obj["a"] = 10; // Allowed
obj["newKey"] = 3; // Allowed
obj = { "a": 1, "b": 2, "newKey": 3 }; // NOT ALLOWED

Spread Operator

We could write our decrementEach function so that it takes a list of parameters rather than an array using a "spread" operator (Ruby calls this a "splat" operator). Here's what the method would look like:

export function decrementEach(...array) {
    return array.map((element) => element - 1);
}

console.log(decrementEach(1, 2)); // => [0, 1]

As shown above, to call the method, we can no longer pass an array. We have to pass a list of parameters. We would have to rewrite our test to look like this:

test/index_spec.js
// ...

describe('index.js', () => {
    // ...

    describe('decrementEach()', () => {
        it('decreases each element', () => {
            expect(decrementEach(1, 2, 3)).to.eql([0, 1, 2]);
        });
    });
});

We can also use the spread operator to call a method that takes a list of parameters when we have those parameters in an array:

const arr = [1, 2, 3];
console.log(decrementEach(...arr)); // => [0, 1, 2]

Eloquently is a recruiting firm. We also host workshops that teach web development and career skills. If you are looking for a job or are interested in joining our web development workshops, please contact us!

We put together some guides for participants in our workshops. Feel free to use them. If you see any errors, please submit an issue on our github repository.

These guides are a work in progress. If you see any errors or have a suggestion for a better way to do something, please let us know.