Test Driven Development

in JavaScript

 

by Brandon Konkle (@bkonkle)

Freelance Web Developer

http://konkle.us

What is TDD?

 

First we write a test.

 

Then we run it and make sure it fails as expected.

 

Only then do we write some code.

But, why???

Programming is hard.

We try to be smart and we work really hard, so we often succeed.

 

Imagine programming is like pulling up a bucket of water from a well. When the well isn't too deep and the bucket isn't too full, it's easy. After a while, though, you're going to get tired.

TDD is like having a ratchet that lets you save your progress, take a break, and make sure you never slip backwards. That way you don’t have to be smart all the time.

What if it doesn't have to be this way?

TDD helps you write better code!

When you write your tests first, you tend to break your code into smaller chunks with well-defined inputs and outputs.

 

These smaller chunks are much easier to reuse and compose later.

When writing your tests first, it's easier to:

 

  • Actually have full test coverage!
  • Write clean, well-structured code.
  • Plan ahead for how your code will be used.
  • Clearly define your code's responsibilities.
  • Identify things that could break and find
    solutions ahead of time.

 

With great test coverage in place, you can be confident when you need to come back later and make big changes. You'll also be providing a great resource to other developers who maintain your code later.

Okay, fine. How?

Unit Tests

Tests that evaluate small pieces of functionality by evaluating the result given a certain input or state.

The code is reasonably isolated from other functionality, with external modules often replaced with mock objects so they don't affect the outcome.

Functional Tests

Tests that describe how a user interacts with your application and check the results based on what the user should see.

All of the components of your app are tested together, fully integrated.

This is usually accomplished through something like Selenium.

For this presentation, we'll focus on unit tests.

JS Test Runners

Ava

"Futuristic" runner written by Sindre Sorhus for super speed via multi-process parallel testing.

Jasmine

Created by Pivotal Labs for Ruby/Rails integration.

Mocha

Created by TJ Holowaychuk to be very flexible and easy to use for Node developers.

Tape

Written by substack to be lightweight, global-free, and very modular.

The following examples use Mocha, but the concepts can generally be applied across all runners.

A Simple Test Suite

describe('todoService', () => {

  before(() => {
    // Setup code that runs before the test suite is executed
  })

  describe('getTodos()', () => {

    before(() => {
      // Setup code that runs before the getTodos() tests are executed
    })

    it('retrieves all todos from the API', () => {
      const result = getTodos()
      expect(result).to.deep.equal({id: 1, title: 'Write tests'})
    })
    
  })
  
})

Results

Reporters

Lifecycle Hooks

before(() => {
  // Setup code that runs before the tests within the current describe block
})

beforeEach(() => {
  // Setup code that runs before each test
})

after(() => {
  // Teardown code that runs after the tests within the current describe block
})

afterEach(() => {
  // Teardown code that runs after each test
})

Asynchronous Testing

it('retrieves all todos from the API', done => {
  getTodos(result => {
    expect(result).to.deep.equal({id: 1, title: 'Write tests'})
    done()
  })
})

it('retrieves all todos from the API', () => {
  const promise = getTodos()

  return promise.then(result => {
    expect(result).to.deep.equal({id: 1, title: 'Write tests'})
  })
})

it('retrieves all todos from the API', () => {
  const promise = getTodos()
  return expect(result).to.eventually.deep.equal({id: 1, title: 'Write tests'})
})

Mocking

import chai from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'                                          ;

chai.use('sinon-chai')

describe('myModule', () => {

  describe('myFunction()', () => {

    it('calls the given callback with an array', () => {
      const callback = sinon.spy()

      myFunction(callback)
      
      expect(callback).to.have.been.calledOnce

      const call = callback.firstCall
      expect(call.args[0]).to.be.an.instanceof(Array)
    })
    
  })
  
})

Use it to:

  • Isolate distinct functionality
  • Remove dependencies on external code
  • Provide predictable responses for internal or external interaction

Ajax Interception

import nock from 'nock'                                                                                      ;

const platform = nock('http://localhost:8080')                                                   ;

describe('getPostTitles()', () => {

  it('requests posts from the platform and returns just the titles', () => {
    platform.get('/posts').reply(200, {id: 1, title: 'My Post', author: 'Me'})

    const results = getPostTitles()

    expect(results).to.equal(['My Post'])
  })
  
})

Dependency Injection

import {isResponsive} from '../path/to/codingTools'              ;

export function myFunction() {
  // Code all the things!
}
const isResponsiveStub = sinon.stub().returns(true)

const {myFunction} = proxyquire('../src/myModule', {
  '../path/to/codingTools': {isResponsive: isResponsiveStub}
})

// Test all the things!

myModule.js 

testMyModule.js →

What if you need isResponsive() to return true every time it's called?

So, what do I test?

As much as possible!

 

Identify discrete units of functionality and write tests that only test one thing.

 

For every function, think through the possible inputs and expected outputs. Test each case individually.

 

If you find yourself mocking or injecting a lot, you may want to refactor.

TDD is a discipline, and that means it’s not something that comes naturally; because many of the payoffs aren’t immediate but only come in the longer term, you have to force yourself to do it in the moment.

 

Like a kata in a martial art, the idea is to learn the motions in a controlled context, when there is no adversity, so that the techiques are part of your muscle memory.

 

The danger is that complexity tends to sneak up on you, gradually. 

React and Redux

 

Test action creators to ensure they return the right actions.

 

Test reducers to make sure the state is appropriately modified.

 

Test event handlers on components to verify that they are correctly handled.

(Use Function.call() to attach test props.)

 

Don't test propType validation - React already covers that functionality in their test suite.

What about JSX?

import {shallow} from 'enzyme'
import React from 'react'
import sinon from 'sinon'

describe('<MyComponent />', () => {

  it('renders children when passed in', () => {
    const wrapper = shallow(
      <MyComponent>
        <div className="unique" />
      </MyComponent>
    )
    expect(wrapper.contains(<div className="unique" />)).to.equal(true)
  });

  it('simulates click events', () => {
    const onButtonClick = sinon.spy()
    const wrapper = shallow(
      <Foo onButtonClick={onButtonClick} />
    )
    wrapper.find('button').simulate('click')
    expect(onButtonClick.calledOnce).to.equal(true)
  })

})

It's been a long journey, but effective JSX unit testing is finally here thanks to airbnb's Enzyme library.

A rapid feedback loop is ideal.

Test Driven Development in JavaScript

By Brandon Konkle

Test Driven Development in JavaScript

  • 2,136