28
PAINLESS JAVASCRIPT UNIT TESTING

Painless Javascript Unit Testing

Embed Size (px)

DESCRIPTION

Some javascript testing patterns that has made writing tests at Refinery29 more maintainable. Covered in this presentation is userContext (or let) in Jasmine, lazy evaluation and shared behavior.

Citation preview

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!