Unit Tests
Rigor
"the process of being thorough, accurate, or exhaustive"
Professionalism
Profession-alism
Doing the right thing by your profession, rather than the right thing for yourself
Rigor: the process of being thorough, accurate, or exhaustive
Pair
Programming
Code
Reviews
Unit
Tests
Why write unit tests?
Projects that include unit tests:
- Safer to build and deploy
- Easier to refactor
- Faster to learn
- Self-documenting API
Projects that include unit tests:
- deceptive sense of safety
- security
Can also be
It's good to have a sense of safety, but not a deceptive sense of safety.
What is a "unit"?
- A Function
- Single purpose
- 1 input => 1 output
let unit = (input) => {
//...
return output;
};
What is a "test"?
Call a function with mock data and observe it's output
Like other forms of testing, unit tests measures success against criteria
Writing "units"
- Functions become
- smaller
- reusable
- single-purpose
Functions become... functional
pure functions
integrations
const add = (a, b) => {
return a + b;
};
const myAlgo = (a, b) => {
//...
return result;
}
const pageLoad = () => {
A.init();
B.init();
// do all the things
};
const $ = () => {
return {
//all the stuff
}
}
dirty
A unit test should have the following properties:
- It should be automated and repeatable.
- It should be easy to implement.
- Once it’s written, it should remain for future use.
- Anyone should be able to run it.
- It should run at the push of a button.
- It should run quickly.
To borrow from mathematics
When functions are unknown, they can be understood and approximated by modeling input to output values
Requirement
Iterative
Development
Test
Driven
Development
Basics of testing
import cookieToObject from '../cookieToObject.js';
const getUserSettings = (cookie) => {
const data = cookieToObject(cookie);
return {
autoPlay: data.autoPlay ? data.autoPlay : false,
profileColor: data.profileColor ? data.profileColor : '#fff'
};
};
- describing the `getUserSettings` function
- it should
- return an object with data that matches their settings
- return defaults if they don't have settings
- it should
1. Examine Function
2. Describe using natural language
Basics of testing
describe('getUserSettings', () => {
it('returns default data if nothing is passed in', () => {
data = getUserSettings();
assert.false(data.autoPlay);
assert.equal(data.profileColor, '#fff');
});
if('returns correct settings' => {
data = getUserSettings('autoPlay=true&profileColor=#00f');
assert.equal(data.profileColor, '#00f');
assert.true(data.autoPlay);
});
});
- describing the `getUserSettings` function
- it should
- return an object with data that matches their settings
- return defaults if they don't have settings
- it should
Testing Technology
In these example
A framework which will execute and report tests, can run phantom.js, chrome, babel, browserify, etc
Libraries which you write the actual test code in. Supports should and assert syntax
Complex Behavior
Click loads
image flyout
Image caption loaded
from data attribute
Image flyout should be responsive & resized to fit container
Arrows should
navigate
through gallery
"x" should
close flyout
Large image
loaded from
data attribute
All that functionality & complexity boils down to:
DOM manipulation
- Writing dom code in a way that can be unit tested
- writing the unit tests themselves
Photo Gallery module example
let gallery = () => {
document.querySelector('.flyout__close').addEventListener('click', (evt) => {
let flyout = document.querySelector('.flyout');
flyout.parentNode.removeChild(flyout);
});
};
export default {
gallery
};
Photo Gallery test example
import {assert} from 'chai';
import {gallery} from './index.js';
describe('photoGallery', () => {
describe('close button', () => {
it('removes the drawer when clicked', (done) => {
});
});
});
let gallery = () => {
document.querySelector('.flyout__close').addEventListener('click', (evt) => {
let flyout = document.querySelector('.flyout');
flyout.parentNode.removeChild(flyout);
});
};
export default {
gallery
};
Refactor photoGallery function
import {assert} from 'chai';
import {gallery} from './index.js';
describe('photoGallery', () => {
describe('close button', () => {
it('removes the drawer when clicked', (done) => {
});
});
});
let gallery = ({context = document} = {}) => {
context.querySelector('.flyout__close').addEventListener('click', (evt) => {
let flyout = context.querySelector('.flyout');
flyout.parentNode.removeChild(flyout);
});
};
export default {
gallery
};
Pass context to module, default to `document`
Use `context` in place of document
Modify unit test
import {assert} from 'chai';
import {gallery} from './index.js';
let fragment = document.createElement('div');
fragment.innerHTML = `<div class="flyout">
<a class="flyout__close" href="#close-flyout">X Close</a>
</div>`;
describe('photoGallery', () => {
describe('close button', () => {
it('removes the drawer when clicked', (done) => {
gallery({context: fragment});
});
});
});
let gallery = ({context = document} = {}) => {
context.querySelector('.flyout__close').addEventListener('click', (evt) => {
let flyout = context.querySelector('.flyout');
flyout.parentNode.removeChild(flyout);
});
};
export default {
gallery
};
Mock the dom fragment
Pass the fragment
to thegallery function
Modify unit test
import {assert} from 'chai';
import {gallery} from './index.js';
let fragment = document.createElement('div');
fragment.innerHTML = `<div class="flyout">
<a class="flyout__close" href="#close-flyout">X Close</a>
</div>`;
describe('photoGallery', () => {
describe('close button', () => {
it('removes the drawer when clicked', () => {
gallery({context: fragment});
let closeBtn = fragment.querySelector('.flyout__close');
closeBtn.addEventListener('click', (evt) => {
assert.isFalse(!!fragment.querySelector('.flyout'));
})
closeBtn.dispatchEvent(new Event('click'));
});
});
});
export default function photoGallery({context = document} = {}){
context.querySelector('.flyout__close').addEventListener('click', (evt) => {
let flyout = context.querySelector('.flyout');
flyout.parentNode.removeChild(flyout);
});
};
Programmatically "click" on the close button
Verify that the `.flyout` is gone after the click
Modify unit test
let fragment document.createElement('div');
fragment.innerHTML = `<div class="flyout">
<a class="flyout__close" href="#close-flyout">X Close</a>
</div>`;
gallery = photoGallery({context: fragment});
describe('photoGallery', () => {
describe('close button', () => {
it('removes the drawer when clicked', (done) => {
closeBtn = fragment.querySelector('.flyout__close');
closeBtn.addEventListener('click', (evt) => {
assert.false(fragement.querySelector('.flyout'));
done();
})
closeBtn.dispatchEvent(new Event('click'));
});
});
});
let gallery = ({context = document} = {}) => {
context.querySelector('.flyout__close').addEventListener('click', (evt) => {
let flyout = context.querySelector('.flyout');
flyout.parentNode.removeChild(flyout);
});
};
export default {
gallery
};
Pass `done` to the test, then call it when done with an asynchronous operation
What have we accomplished?
Taken a "dirty" function, abstracted the DOM, simulated user input, observed results
The close function can now be tested automatically, and a build can notify you if it fails
Refactoring Functions to be Unit Testable
Problem: Inaccessible Functions
Solution: Expose Api
export default function init = () => {
inaccessible();
};
let function inaccessible = () => {
//...
};
let init = () => {
accessible();
};
let accessible = () => {
//...
};
export default {
init,
accessible
}
Refactoring Functions to be Unit Testable
Problem: Direct `document` access
Solution: abstract the dom
let addClass = (selector, class) => {
[...document.querySelectorAll(selector)].forEach((elem) => {
elem.classList.add(class);
});
};
let addClass = ({
context = document,
selector,
class} = {}) => {
[...context.querySelectorAll(selector)].forEach((elem) => {
elem.classList.add(class);
});
};
Refactoring Functions to be Unit Testable
Problem: Functions aren't discrete (anonymous functions)
Solution: Extract functionality into units
let myComplexFunction = (data) => {
//sort data
data.sort((a,b) => {
//...
});
//transform data
data = data.map((d)=>{
//...
});
return data;
};
let customSort = (a,b) => {
//...
};
let customMap = (elem) => {
//...
};
let myComplexFunction = (data) => {
//sort data
data.sort(customSort);
//transform data
data = data.map(customMap);
};
Sum Up
- Rigor
- Benefits of Unit Tests
- Pure functions <==> Integration
- Writing unit tests for complex behavior
- Refactoring for Unit Testability
Thanks.
Unit Tests
By Michael Jasper
Unit Tests
- 754