Testing
Front-end Code
the
Right Way
By: Matias Niemelä
matias [at] yearofmoo [dot] com
Slides & Code
Slides: yom.nu/fitc-spotlight-testing-slides
Code: yom.nu/fitc-spotlight-testing-code
What are we here to learn?
The importance of testing software
How to write & run tests
Good code vs bad code
Unit Testing & Integration Testing
Why test software?
We all test our code somehow
But untested software is very limited
We have no confidence to refactor
Or remove code or extend things
Who tests all their code?
Does refreshing the browser count?
But wait. I don’t need to test!
Programming is easy
I know what I’m doing
I can keep it all in my head
Bugs. Issues. Tracking Code.
Testing is essential with source-control
When working on a team
When other people are working on a new feature
While others are still working on older code
Example 1: Underscore.js w/o tests
function reduce(array, exp) {
var value = array[0];
for(var i=1;i<array.length;i++) {
value = exp(value, array[i]);
}
return value;
}
reduce([1,2,3,4,5], function(x,y) {
return x + y;
});
Things are getting more complex
Programmer 1 saves and then Programmer 2 builds on top
# Programmer 1 - save the code
git commit -m "reduce() feature complete"
# Programmer 2 - jump out
git checkout -b filter_feature
Example 1: More code
function filter(arr, fn) {
var values = [];
for(var i=0;i<arr.length;i++) {
if (fn(arr[i])) {
values.push(arr[i]);
}
}
return values;
}
More code
// he changes around the attributes
function filter(fn, arr) { ... }
function reduce(fn, arr) { ... }
function map(fn, arr) { ... }
Then we merge
Programmer 2 then merges the code onto master
# Programmer 2 - save the code
git commit -m "filter() feature complete"
git commit -m "reordered the function params"
# Programmer 2 - jump out
git checkout master
git merge filter_feature
The collision?
Programmer 1 then builds on top of master
Still using the old parameter ordering
How does he know the code is broken?
There are no tests
Benefits of tests
We always know when things are broken
We have less production code
Our code can be refactored easier
Cleaner, more comprehensible, more maintainable
The chicken or the egg?
Will our code be better without tests?
Because we have more time
Or will we figure out the bugs earlier?
Since we have tests
Challenges of testing
Setting up the test runner/environment
The discipline of writing tests
Knowing when you’re in the clear
Writing good tests
The Process
The code is written
Tests are written & executed
We fix bugs and refactor
Repeat
So what are the levels of a web app?
HTML
JS
CSS
Backend Code
JS is the most important piece
We stick to doing unit testing primarily
Unit tests are cheap and fast
When build properly they can last a long time
Test Env: Karma
Used to run tests on browsers
Automatically runs tests on start
Or watches them for changes
We use this to power our unit tests
Setting up Karma
// Install NodeJS and NPM
// Then install Karma
npm install karma -g
// Setup karma on your code
karma init
// Start Karma
karma start
Running Karma
Test Env: Jasmine
Used to describe the tests
Each description creates a sentence
Tests are designated with "it" (called specs)
Pending tests are cool as well
An all-inclusive Jasmine Test
describe("when testing a FEATURE", function() {
it("should PERFORM in THIS WAY", function() {
expect(someOperation()).toBe(someValue); //numbers
expect(someOperation()).toEqual(someValue); //strings, arrays
expect(someOperation()).toBeLessThan(10); //strings, arrays
expect(someOperation()).toBeFalsy();
expect(someOperation()).toBeTruthy();
expect(someOperation()).toBeOneOf('a','b','c');
expect(someOperation()).not.toEqual(someValue);
});
});
Karma + Jasmine
Let's see an example in action
Jasmine Matchers
Matchers are the essentials
They place expectations on the code
And tell the test runner when it has failed
The goal is to make the test easy to read
Jasmine Matchers
describe("the email dispatcher", function() {
it("should only send if subject + message exist", function() {
var dispatcher = new EmailDispatcher();
dispatcher.subject = "My Special Guy";
dispatcher.send();
expect(dispatcher).not.toHaveSentEmail();
dispatcher.message = "My special message for my special guy";
dispatcher.send();
expect(dispatcher).toHaveSentEmail();
});
});
A Failed Test
How to go about unit testing
Each unit test just tests one thing
We need to test the "sticky points"
(The tasks that are best likely to stay)
Example 2: Username Validator
Validations: digits, lower, upper, special, no-spaces
How many sticky points are there?
around say 5 + 1?
Let’s make that many tests
Example 2: Username Validator
function usernameValidator(input) {
return /[0-9]+/.test(input) &&
/[a-z]+/.test(input) &&
/[A-Z]+/.test(input) &&
/\W+/.test(input) &&
!/\s+/.test(input);
}
Example 2: Tests
it('should require digits');
it('should require lowercase letters');
it('should require uppercase letters');
it('should require special characters');
it('should require special characters');
Example 2: Tests
it('should require digits', function() {
usernameValidator('u$eRname').toBeFalsy();
usernameValidator('u$3Rname').toBeTruthy();
usernameValidator('u$3Rnam5').toBeTruthy();
});
Rule of Thumb
Small tests
Build the shared test input in a before each
More tests for a subroutine == more complexity
This is getting out of hand
Even though we have unit tests
There are way too many questions
Each test is too informed about the others
Best to break things apart
Time to Refactor
We should split apart each inner feature
But we should not change the tests
The good thing is we can refactor
Let’s break apart each validator input functions
Example 2: Tests
function usernameValidatorRefactored(input) {
return numericValidator(input) &&
lowerCaseValidator(input) &&
upperCaseValidator(input) &&
specialCharactersValidator(input) &&
nonSpaceValidator(input);
}
Example 2: Tests
function numericValidator(input) {
return /[0-9]+/.test(input);
}
function upperCaseValidator(input) {
return /[A-Z]+/.test(input);
}
function specialCharactersValidator(input) {
return /\W+/.test(input);
}
Now how can we improve the tests?
We can stub out each function now
And not have to worry about any combination
The goal is to mock out the dependencies
Just don’t overdo it
Example 2: Tests
describe("UsernameValidator", function() {
var validators = {};
beforeEach(function() {
//numericValidator will now always return true
validators.numeric = stub('numericValidator', true);
//validators.numeric restores the old function back
validators.lower = stub('lowerCaseValidator', true);
validators.upper = stub('upperCaseValidator', true);
validators.special = stub('specialCharactersValidator', true);
validators.noSpace = stub('nonSpaceValidator', true);
});
Example 2: Tests
it("should require digits", function() {
validators.numeric();
expect(usernameValidatorRefactored("username")).toBeFalsy();
expect(usernameValidatorRefactored("us3rname")).toBeTruthy();
});
it("should require lowercase letters", function() {
validators.lower();
expect(usernameValidatorRefactored("USERNAME")).toBeFalsy();
expect(usernameValidatorRefactored("USERnAME")).toBeTruthy();
});
Mocking / Stubbing and Spies
Stubbing is when a function returns a fake value
Mocking is when more complex data is returned
Spies capture (and maybe mutate) data
Debugging
Sometimes tests break and we need to debug
Use iit or xit to focus on or filter out tests
Debugging
//this test will be the only test that is run
iit("should require digits", function() {
validators.numeric();
debugger;
expect(usernameValidatorRefactored("username")).toBeFalsy();
expect(usernameValidatorRefactored("us3rname")).toBeTruthy();
});
//this test will be skipped
xit("should require lowercase letters", function() { });
testing async code
Our test should be 100% synchronous
But how do we manage async code?
The trick is to capture the async call
We ALWAYS want to mock out external code
Async Code
it("should download the user data", function() {
mockFakeHttpResponse("http://yearofmoo.com/index.html", function() {
return "... welcome to the index page ...";
});
http("http://yearofmoo.com/index.html", function(html) {
expect(html).toEqual("... welcome to the index page ...");
});
});
Promise-based code
The future of async JS is promises
Promises introduce task control
A micro task queue is a series of events
With promises we can manage order
ES6 and the future
ES6 is the new version of javascript
We have access to modules & promises
We can easily mock modules
ES6 Imports
export function numericValidator(input) {
return /[0-9]+/.test(input);
}
export function lowerCaseValidator(input) {
return /[a-z]+/.test(input);
}
export function upperCaseValidator(input) {
return /[A-Z]+/.test(input);
}
ES6 Imports
import * as validators from './username-validators'
stub(validators, 'lowerCaseValidator', true);
stub(validators, 'upperCaseValidator', true);
stub(validators, 'numericValidator', true);
Best to use a framework
Find a framework to suit your needs
Make sure it has a testability story
The easier the tests then the better the code
Pro tips for JS unit testing
No global variables
Functions should always do one thing
Small files, many functions
Functions = verbs, variables = nouns
Pro tips for JS code overall
Functions at the bottom
Event listeners at the top
Use closures for shared data
Split setters from getters
Beyond unit testing
Unit tests can’t do everything
But we should always try to unit test it all
However we use integration testing when we can’t
HTML / CSS can’t be unit tested alone
Integration testing
A web driver does the trick
It can test out the behaviour of a page
And tell us when things break
Let’s use protractor
Protractor
This testing tool is designed for Angular
But we can get away with using it for non Angular stuff
The goal here is to behave a the user
And then to test against a series of outcomes
Installing Protractor
// Install NodeJS and NPM
// Then install Protractor
npm install protractor -g
// Setup karma on your code
webdriver-manager update
// Start Protractor and WebDriver (in two tabs)
protractor ./protractor.conf.js
webdriver-manager start
Configuring Protractor
Create a file called protractor.conf.js
exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['integration/testSpec.js']
}
Protractor without Angular
We need to add this at the top of each test file
beforeEach(function() {
return browser.ignoreSynchronization = true;
});
Example 3: Protractor
describe('username validator page', function() {
it('should validate the username', function() {
browser.get('http://localhost:8888');
element(by.css('#input-username')).sendKeys('username');
element(by.css('[type="submit"]')).click();
expect(element(by.css('.error')).getText())
.toEqual('You have entered an invalid username');
});
});
Example 3: Protractor
Make sure the website is running at localhost:8888
<form id="username-form" action="/validate.php">
<div class="field">
<label>Username:</label>
<input type="text" id="input-username" />
</div>
<input type="submit" />
</form>
Example 3: Protractor
it('should send a request to the server and update the page', function() {
browser.get('http://localhost:8888');
element(by.css('#input-username')).sendKeys('u$3rnAme');
element(by.css('[type="submit"]')).click();
expect(element(by.css('.error')).isPresent()).toBeFalsy();
expect(element(by.css('.loading')).getText())
.toEqual('Loading...');
var messageElement = element(by.css('.success'));
browser.wait(function() {
return messageElement.isPresent();
});
expect(messageElement.getText())
.toEqual('You have registered successfully');
});
Integration tests as a whole
While they’re super powerful and fun
They break easily
Since any structural change can be the culprit
So always name your CSS or ID values intelligently
Thanks!