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,205