Testing: TDD, Mocha, and Chai

Learning Goals:

  • Review the what and why of test driven development
  • Examine the structure of a test
  • Discuss the testing lifecycle
  • Practice writing tests

Vocab

  • TDD Test Driven Development / Design
  • Assertion An expression containing some testable logic
  • Assertion Library A package of assertion functionality. Usually distinct from a Testing Framework
  • Testing Framework A library that determines how tests are organized and executed
  • Red Green Refactor The process of writing a failing test, making it pass, then refactoring the tests and/or implementation with confidence
  • Test Phases A test that is organized into the phases [Setup, Execution, Assertion, Teardown*

Warmup! Let’s Review TDD!

What is TDD?

TDD, or Test Driven Development, is the concept of writing a series of assertions in a test file BEFORE writing any of the applicable code that supports the tested functionality.

What are some of the benefits of writing tests?

  • Computers can test things faster and more accurately than humans: testing things manually in the browser is tedious, error prone and slow
  • Forces you to slow down and pseudocode: which helps you think more thoroughly about potential pitfalls before you write your code; it’s much easier to course-correct yourself before you right any code than to refactor broken code after it’s been written
  • Provides a blueprint for new developers to see how the codebase should work: if your tests are thorough and well-written, a new developer should be able to hop directly into the test folder and get a solid understanding of how each piece of the codebase works
  • Provides future integrity for your code as you iterate on your application: applications are never done and can always be improved, added to, pivoted, etc. Tests ensure that as we make these changes, we won’t accidentally introduce new bugs
  • Forces you to write more module, SRP-style code: often times you’ll only recognize opportunities to refactor as you go to write tests for you code and find that it’s not testable

Are there any downsides to using TDD?

  • It takes more time to write and maintain your codebase, which slows down development: this can be problematic if you’re working in an environment where meeting deadlines is a top priority (like working in a newsroom)
  • They don’t make the business money: tests aren’t features, and if your company is relying on investors to keep itself going, making progress on the application functionality is going to be of utmost importance

The Main Benefit

The main benefit of Test Driven Development is that you are forced to look at the problem you are solving at a high level and not worry about the little details for implementing your solution.

What Makes Testing Hard?

  • At first, not knowing what to test
  • So many libraries and frameworks to choose from, differing syntax in documentation
  • Difficult to see the benefit until you’ve been saved by a failing test

What Should Be Tested?

When talking about what should be tested, we say that we want to test the outcome or result of a particular piece of code execution. This is an important distinction and can help clarify one of the key pieces of what makes testing hard.

For example, let’s say we have a quiz application that checks a user’s answers and adds/removes points from their score:

class Question {
  constructor(questionText, correctAnswer, player) {
    this.questionText = questionText;
    this.correctAnswer = correctAnswer;
    this.player = player;
  }

  checkAnswer(playerAnswer) {
    if (playerAnswer === this.correctAnswer) {
      this.player.score++
    } else {
      this.player.score--
    }
  }
}

What would we want to verify about the checkAnswer method? What should this method do?

describe('Question Class', () => {
  it('should increment a player score when their answer is correct', () => {

  });

  it('should decrement a player score when their answer is incorrect', () => {

  });
});

Practice

In your journal:

  • Take a look at the following code. What would you want to test about this function?
function reverseWord(word) {
  let letters = word.split('');
  letters.reverse();
  return letters.join('');
}

reverseWord('turing'); // gnirut

Reviewing Mocha vs. Chai

In Your Notebook

  • What is the difference between Mocha and Chai?
  • What are each of their responsibilities?

The Answer

Mocha is a testing framework while Chai is an assertion library.

Mocha:

  • Mocha runs on Node.js in your terminal, and can also be run in your browser window
  • Mocha itself is the framework that runs the tests and dictates the syntax of the test block as a whole. This is separate from the assertion library Chai.
describe('unicorn', function() {
  it('should accumulate calories when calling eat', function() {

  });
});

Chai:

  • An assertion is the crucial piece of the test that actually checks that when certain pieces of are code are executed, what we’re getting back is what we expect.
  • Although Chai can be inserted into many different testing frameworks, it works seamlessly with Mocha.

A note about the multiple syntax options provided by Chai

Chai Syntax Libraries

Although there are small differences, all three interfaces can accomplish the same task. As a developer you can choose which version feels best to you. For example, expect provides a function as a starting point for chaining assertions, whereas should extends the Object.prototype to provide a single getter as the starting point. Expect works on node.js and all browsers, while should does not work in Internet Explorer. For today we are going to go with the Expect API.

Structure of a Test

Good tests have Four Phases:

  1. Setup - Setup the conditions required to execute the action on your SUT
  2. Execution - Execute some action on your SUT
  3. Assertion - Assert that the action you did had the results you expect
  4. Tear Down - Clean up any resources you used in your test (this is done automatically the majority of the time)

(Most tests you write will not need the Tear Down phase, but it’s good to know that step is there sometimes)

All of these phases deal with the Subject Under Test (SUT, or just subject).

Look at the following example and read the comments that talk about each line of our test:

// Before anything can happen, we need a describe block to group related tests together
// In this case, the tests within our describe block are all related to the 'Unicorn' class
describe('Unicorn', function() {

  // Next, an 'it block' contains the context of each specific test
  it('should add 100 calories after eating', function() {

    // 1. "Setup"
    // Instantiate an instance of our unicorn
    var unicorn = new Unicorn('Susan');

    // 2. "Execution"
    // Run appropriate functions that execute the behavior indicated by our test title
    unicorn.eat();
    unicorn.eat();
    unicorn.eat();

    // 3. "Assertion"
    // Make an assertion to verify that after executing certain functions, we end up with what we expect
    expect(unicorn.calories).to.equal(300);
  });
});

What makes a good test?

  • Test one thing
  • Do not have control flow (if, when, for) statements
  • Can be used as documentation for the code they test
  • Are clear and easy to read

Testing Practice: Setup, Mocha, and Chai

To practice, let’s kick off a small project to demonstrate how you would utilize Mocha and Chai to write a couple tests.

Getting Started (in pairs)

  • In your terminal, clone down this starter repo and run npm install:
git clone https://github.com/turingschool-examples/our-first-tests
cd our-first-tests
npm install
  • Note what files exist in the tests. Take a look at the package.json file as well, noting devDependencies and the scripts.
  • Move to the /test/Box-test.js file, and import our assertion library.
  • Setup your describe block, and write a basic dummy test (such as expect(true).to.equal(true);) to make sure if everything works correctly.
  • Run npm test to see if your test passes. If not, take note of the error message and try to fix it.

Note

If you run into an error like expect is not defined, think about where expect comes from and how you can access it.

The Answer (Only click if you get stuck or have finished)

// test/Box-test.js  

const chai = require('chai');
const expect = chai.expect;

describe('Box', function() {
  it('should return true', function() {
    expect(true).to.equal(true);
  });
});

Running npm test should result in:

Box
  ✓ should return true


1 passing (10ms)  

Testing Practice: Iteration 1

Obviously this test isn’t doing anything helpful, but we know our files are wired up. Let’s add some more interesting tests. Let’s pretend we just received a spec, and the first iteration looks something like this:

Iteration 1

  • You should have a Box constructor which has a default height and width of 100.
  • User should be able to pass in specific height and widths if they so choose.
  • You should be able to calculate the area of your box using the method .area().
  1. Start with writing the tests. Note that you’ll need to import a Box class and create a new instance of it for each test. Your tests will fail when you run npm test.
  2. Now work on the implementation in the Box.js file. Feel free to add .skip to your tests so that you can focus on one at a time.

Test Solution

// test/Box-test.js

const chai = require('chai');
const expect = chai.expect;

const Box = require('../src/Box');

describe('Box', function() {
  it('should return true', function() {
    expect(true).to.equal(true);
  });

  it('should have a default height and a width', function() {
    var box = new Box();

    expect(box.height).to.equal(100);
    expect(box.width).to.equal(100);
  });

  it('should have take a height and a width as arguments', function() {
    var box = new Box(50, 40);

    expect(box.height).to.equal(50);
    expect(box.width).to.equal(40);
  });

  it('should calculate its area', function() {
    var box = new Box(30, 30);

    expect(box.area()).to.equal(900);
  })
});

Implementation Solution

// Box.js  
class Box {
  constructor(height = 100, width = 100) {
    this.height = height;
    this.width = width;
  }

  area() {
    return this.height * this.width;;  
  }
}

module.exports = Box;

Testing Practice: Iteration 2

Let’s continue to practice adding more iterations following the TDD process from before. Implement iteration 2 for our box per the spec outlined below.

Iteration 2

  • You should be able to increase the width by a provided value. ie: box.increaseWidth(10)
  • You should be able to increase the height of your box by a provided value ie: box.increaseHeight(10)

Testing Practice: Iteration 3

Implement iteration 3 for our box per the spec outlined below;

Iteration 3

  • Refactor your code so that instead of having increaseWidth and increaseHeight methods, you can have a single method to do both jobs ie: box.increment(10, 'height') or box.increment(10, 'width')

Checks for Understanding

  • What is the difference between Mocha and Chai?
  • What is the structure of a test?
  • What makes a test good?

Further Reading

Lesson Search Results

Showing top 10 results