Testing Front-end Code the Right Way

By: Matias Niemelä

http://www.yearofmoo.com
matias [at] yearofmoo [dot] com
@yearofmoo

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 &amp; 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!

Twitter: @yearofmoo

Website: www.yearofmoo.com

Email: matias [at] yearofmoo [dot] com