Transcript
Page 1: Painless Javascript Unit Testing

PAINLESS JAVASCRIPT UNIT TESTING

Page 3: Painless Javascript Unit Testing

Setup

Introduction Structure Patterns Resources

Page 4: Painless Javascript Unit Testing

Setup

Introduction Structure Patterns Resources

Page 5: Painless Javascript Unit Testing

Setup

Introduction Patterns ResourcesStructure

Page 6: Painless Javascript Unit Testing

Setup

Introduction Structure Patterns Resources

Page 7: Painless Javascript Unit Testing

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

Page 8: Painless Javascript Unit Testing

IntroductionPairing How many times have you been here?

Page 9: Painless Javascript Unit Testing

IntroductionPairing Warning: They may laugh at you.

Page 10: Painless Javascript Unit Testing

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.

Page 11: Painless Javascript Unit Testing

IntroductionProduct As close to native as possible. http://www.refinery29.com

Page 12: Painless Javascript Unit Testing

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

Page 13: Painless Javascript Unit Testing

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

Page 14: Painless Javascript Unit Testing

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

Page 15: Painless Javascript Unit Testing

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

Page 16: Painless Javascript Unit Testing

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

Page 17: Painless Javascript Unit Testing

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');

});

});

});

Page 18: Painless Javascript Unit Testing

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');

});

});

});

Page 19: Painless Javascript Unit Testing

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...

Page 20: Painless Javascript Unit Testing

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';

});

// ...

Page 21: Painless Javascript Unit Testing

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');

});

// ...

Page 22: Painless Javascript Unit Testing

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!

Page 23: Painless Javascript Unit Testing

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);

});

});

});

});

Page 24: Painless Javascript Unit Testing

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...

Page 25: Painless Javascript Unit Testing

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);

});

});

}

});

});

Page 26: Painless Javascript Unit Testing

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

Page 28: Painless Javascript Unit Testing

Resources

Links

● Our Blog: http://j.mp/R29MobileBlog

● Travis’ Gist on userContext: http://bit.ly/BetterJasmine

● Slides:http://bit.ly/JSStackUp

Thanks!