PAINLESS JAVASCRIPT UNIT TESTING
The Presenters
@traviskaufman @benmidi
Setup
Introduction Structure Patterns Resources
Setup
Introduction Structure Patterns Resources
Setup
Introduction Patterns ResourcesStructure
Setup
Introduction Structure Patterns Resources
Introduction
Team Bicycle
● A Director of Product and Two developers
● Behavior Driven Development and Pairing
● Launched the new mobile web experience in 5 months
● 1000 Unit Tests and 200 Acceptance Tests
IntroductionPairing How many times have you been here?
IntroductionPairing Warning: They may laugh at you.
Introduction
Team Bicycle
● A Director of Product and Two developers
● Behavior Driven Development and Pairing
● Launched the new mobile web experience in 5 months
● 1000 Unit Tests and 200 Acceptance Tests
A team built for speed.
IntroductionProduct As close to native as possible. http://www.refinery29.com
Structure Grunt sets up tasks for running tests, example: grunt specs:debug
Gruntfile.js # Grunt for automating test runs
karma.conf.js # Karma for unit testing (Chrome for debugging, PhantomJS for CI builds in Jenkins)
app/ | js/| | models/| | **/*.js| | views/| | **/*.jsspec/| js/| | models/| | **/*_spec.js| | views/| | **/*_spec.js
Structure Karma sets up our testing environments. Great for running tests on actual devices.
Gruntfile.js # Grunt for automating test runs
karma.conf.js # Karma for unit testing (Chrome for debugging, PhantomJS for CI builds in Jenkins)
app/ | js/| | models/| | **/*.js| | views/| | **/*.jsspec/| js/| | models/| | **/*_spec.js| | views/| | **/*_spec.js
Structure The spec/ directory mirrors app/
Gruntfile.js # Grunt for automating test runs
karma.conf.js # Karma for unit testing
app/ | js/| | models/| | **/*.js| | views/| | **/*.jsspec/| js/| | models/| | **/*_spec.js| | views/| | **/*_spec.js
Structure Inside of spec/ is the helpers/ directory for global setup/tear down, mocks, convenience functions and custom Jasmine matchers.
Gruntfile.js # Grunt for automating test runs
karma.conf.js # Karma for unit testing
app/| js/| | models/| | **/*.js| | views/| | **/*.jsspec/| helpers/| | *.js # Helper files loaded in Karma| js/| | models/| | **/*_spec.js| | views/| | **/*_spec.js
Structure Our rspec acceptance tests also live in spec/
Gruntfile.js # Grunt for automating test runs
karma.conf.js # Karma for unit testing
app/| js/| | models/| | **/*.js| | views/| | **/*.jsspec/| helpers/| | *.js # Helper files loaded in Karma| js/| | models/| | **/*_spec.js| | views/| | **/*_spec.js| features/| | *_spec.rb| | spec_helper.rb| | support/| | **/*.rb
PatternsDRY it up
describe('views.card', function() {
var model, view;
beforeEach(function() {
model = {};
view = new CardView(model);
});
describe('.render', function() {
beforeEach(function() {
model.title = 'An Article';
view.render();
});
it('creates a "cardTitle" h3 tag set to the model\'s title', function() {
expect(view.$el.find('.cardTitle')).toContainText(model.title);
});
});
describe('when the model card type is "author_card"', function() {
beforeEach(function() {
model.type = 'author_card';
view.render();
});
it('adds an "authorCard" class to it\'s $el', function() {
expect(view.$el).toHaveClass('authorCard');
});
});
});
PatternsUsing this
describe('views.card', function() {
beforeEach(function() {
this.model = {};
this.view = new CardView(this.model);
});
describe('.render', function() {
beforeEach(function() {
this.model.title = 'An Article';
this.view.render();
});
it('creates a "cardTitle" h3 tag set to the model\'s title', function() {
expect(this.view.$el.find('.cardTitle')).toContainText(this.model.title);
});
});
describe('when the model card type is "author_card"', function() {
beforeEach(function() {
this.model.type = 'author_card';
this.view.render();
});
it('adds an "authorCard" class to it\'s $el', function() {
expect(this.view.$el).toHaveClass('authorCard');
});
});
});
PatternsUsing this
Jasmine’s userContext (aka this)
● Shared between before/afterEach hooks and tests (including nested tests)
● Cleaned up after every test
● Removes the need for keeping track of variable declarations
● Removes problems that may occur due to scoping and/or hoisting issues
● No global leaks
● Clearer meaning within tests
● Leads the way to...
PatternsLazy Eval
beforeEach(function() {
this.let_ = function(propName, getter) { // need to use "_" suffix since 'let' is a token in ES6
var _lazy;
Object.defineProperty(this, propName, {
get: function() {
if (!_lazy) {
_lazy = getter.call(this);
}
return _lazy;
},
set: function() {},
enumerable: true,
configurable: true
});
};
});
Lazy Evaluation (a la Rspec’s let)
describe('.render', function() {
beforeEach(function() {
this.let_('renderedView', function() {
return this.view.render();
});
this.model.title = 'An Article';
});
// ...
PatternsLazy Eval
describe('views.Card', function() {
beforeEach(function() {
this.model = {};
this.view = new CardView(this.model);
});
describe('.render', function() {
beforeEach(function() {
this.model.title = 'An Article';
this.let_('renderedView', function() {
return this.view.render();
});
});
it('creates a "cardTitle" h3 tag set to the model\'s title', function() {
expect(this.renderedView.$el.find('.cardTitle')).toContainText(this.model.title);
});
describe('when the model card type is "author_card"', function() {
beforeEach(function() {
this.model.type = 'author_card'; // no need to re-render the view here!
});
it('adds an "authorCard" class to its $el', function() {
expect(this.renderedView.$el).toHaveClass('authorCard');
});
// ...
PatternsLazy Eval
Lazy Evaluation
● Our render spec, now uses let_
● We only have to call render once, in the getter function, even if we change the model in nested beforeEach blocks
● Reduced code duplication
● Reduced chance of pollution due to side effects
● Smaller file sizes!
● Note: everything here can be used for Mocha as well!
PatternsBehaves Like
describe('Email', function() {
beforeEach(function() {
this.emailAddress = '[email protected]';
this.let_('email', function() {
return new Email(this.emailAddress);
});
});
describe('.validate', function() {
describe('when emailAddress is missing the "@" symbol', function() {
beforeEach(function() {
this.emailAddress = 'someexample.com';
});
it('returns false', function() {
expect(this.email.validate()).toBe(false);
});
});
describe('when emailAddress is missing a domain after the "@" symbol', function() {
beforeEach(function() {
this.emailAddress = '[email protected]';
});
it('returns false', function() {
expect(this.email.validate()).toBe(false);
});
});
});
});
PatternsBehaves Like
Shared Behavior
● Thorough, but lots of copy-pasta code
● If API changes, all tests have to be updated
○ e.g. what if validate changes to return a string?
● RSpec solves this with “shared examples”
● Need something like that here...
PatternsBehaves Like
describe('Email', function() {
beforeEach(function() {
this.emailAddress = '[email protected]';
this.let_('email', function() {
return new Email(this.emailAddress);
});
});
describe('.validate', function() {
shouldInvalidate('an email without an "@" sign', 'someexample.org');
shouldInvalidate('missing a domain', '[email protected]');
//..
function shouldInvalidate(desc, example) {
describe('when emailAddress is ' + desc, function() {
beforeEach(function() {
this.emailAddress = example;
});
it('returns false', function() {
expect(this.email.validate()).toBe(false);
});
});
}
});
});
PatternsBehaves Like
Shared Behavior
● No more duplication of testing logic
● Extremely readable
● Leverage this along with javascripts metaprogramming techniques to create dynamically built suites
● Looks great in the console
● Completely DRY
● Huge win for something like email validation, where there are tons of cases to test
● Prevents having one test with a large amount of assertions
Resources
We’re Hiring
Front-End Engineer | Full Time
QA Engineer | Full Time
Developer Tools & Automation Engineer | Full Time
Junior DevOps Engineer | Full Time
Desktop Support Engineer | Full Time
Back-End Engineer | Full Time
Resources
Links
● Our Blog: http://j.mp/R29MobileBlog
● Travis’ Gist on userContext: http://bit.ly/BetterJasmine
● Slides:http://bit.ly/JSStackUp
Thanks!
Recommended