how to write easy-to-test javascript

Post on 18-Dec-2014

573 Views

Category:

Technology

2 Downloads

Preview:

Click to see full reader

DESCRIPTION

Common anti patterns that make your JavaScript code hard to test, and how to fix them to make it easy.

TRANSCRIPT

Writing Easy-To-Test CodeYnon Perek ynon@ynonperek.com http://ynonperek.com

Problem #1 How do you write hard

to test code ?

Code Flags• Use global state

• Use static methods

• Mix object construction with business logic

• Mixing find-what-i-need logic with business logic

• Write LONG functions

• Use many conditionals

• Dependency hell

• Long inheritance hierarchies

Isolating Logic

TreeWidget

ItemWidget

Button

Data Supplier

jQuery

Parser

Array.sort

Data Object

Isolating Logic

TreeWidget

ItemWidget

Button

Data Supplier

jQuery

Parser

Array.sort

Data Object

Isolating Logic

TreeWidget

ItemWidget

Button

Data Supplier

Test

Isolating Logic

TreeWidget

ItemWidget

Button

Data SupplierTest

Main

The Code

function TreeWidget() { var dataSupplier = new DataSupplier('/music/collection'); }

function TreeWidget(ItemWidget, dataSupplier) { // ...}

If you can isolate it, you can test it

What Can You Test ?colors = ['red', 'blue', 'green', 'yellow', 'cyan', 'magenta']; $('#btn').html('Click Me'); $('#btn').on('click', function() { var idx = $('body').attr('data-color'); idx = Number(idx) + 1 || 0; $('body').attr('data-color', idx); if ( Number(idx) >= 0 ) { $('body').css("background", colors[idx]); } else { $('body').css('background', colors[0]); }});

Dependencies

• Colors array

• DOM structure

• jQuery

Let’s Try This Onefunction ColorChanger(colors_array, $btn_el, $body_el) { var self = this; var _current_color = 0; self.init = function() { $btn_el.html('Click Me'); $btn_el.on('click', self.apply_next_color); }; self.apply_next_color = function() { $body_el.css('backgroundColor', colors_array[_current_color]); _current_color += 1; };} var c = new ColorChanger(colors, $('#btn'), $('body'));c.init();

Now you can easily test:

• Code iterates over all colours

• Code works well on all possible colours array

• Colour iteration is circular

Takeaways

• Refactoring code can make it easier to test

• The goal:

• Isolated logic

• Clear replaceable dependencies

Agenda

• Dependency Injection

• Data / DOM separation

• Component based architecture

• Design Patterns

Dependency Injection

• State all your dependencies at the top

• Separate object creation and lookup from business logic

DI Framework

• A framework that does object creation for you

• Some frameworks also manage object lifecycle

Famous DI

global.myapp.controller( 'Home', ['$scope', '$routeParams', 'Notebooks', function($scope, $routeParams, Notebooks) { // ...}]);

Famous DI

require(["helper/util"], function(util) {});

Vanilla DI

• You don’t really need a framework

Q & A

Data / DOM

JS HTMLDivElement

DOM APIBusiness logic

write

read (event handlers)

Event Handlers

• Get the data

• Call testable handler function

$('#username').on('input', function() { var newValue = this.value; self.checkUsername(newValue);});

Mixing Lookups

$('#btn').on("click", function() { if ( $('#page1').is(':hidden') == true ) { $('#page1').show(); $('#page2').hide(); } else { $('#page1').hide(); $('#page2').show(); }});$('#page1').show();

Non Mixed Versionfunction Toggle(pages) { var active = 0; function toggle() { pages[active].hide(); active = (active + 1) % pages; pages[active].show(); } pages[0].show(); return toggle; } $('#btn').on('click', Toggle([$('#page1'), $('#page2')]));

Testing Non-Mixed Version

• Setup fake dependencies

var FakePage = function() { _visible = false; return { show: function() { _visible = true; }, hide: function() { _visible = false; }, visible: function() { return _visible; } }} ;

Testing Non-Mixed Version• Inject and test

var toggle = Toggle([p1, p2]);expect(p1.visible).to.be.true;expect(p2.visible).to.be.false; toggle();expect(p1.visible).to.be.false;expect(p2.visible).to.be.true;

Mixing Lookups

• Separate lookup code from business logic

• Test interesting parts -> business logic

Components Based Architecture

Guidelines

• Well defined components with a clear API

• Dependencies for each component are injected upon creation

• System is a tree of components

Components

Home Page

Sidebar Content

Reducing Dependencies

• Task: Clicking a menu item in the sidebar should change active item in $content

• Is $content a dependency for $sidebar ?

Code From Sidebar

$('.menu .item').on('click', function() { var item_id = $(this).data('id'); $content.set_active_item(item_id); });

Direct Connection Problems

• It doesn’t scale

• Requires tester to mock many components

Solution: Observer Pattern

• All components share a “hub”

• No direct messages between components

• Easy on the testing

Using Events$('.menu .item').on('click', function() { var item_id = $(this).data('id'); $hub.trigger('active_item_changed', item_id);});

$hub.on('active_item_changed', set_active_item);

Sidebar

Content

Testing Events

for ( var i=0; i < items.length; i++ ) { hub.trigger('active_item_change', i); expect($('#content').html()).to.eq(items[i]); }

The Pattern

JS Observer

• Observer is just a function

• Notify by calling it

Q & A

Code Flags• Use global state

• Use static methods

• Mix object construction with business logic

• Mixing find-what-i-need logic with business logic

• Write LONG functions

• Use many conditionals

• Dependency hell

• Long inheritance hierarchies

Thanks For Listening

• Ynon Perek

• http://ynonperek.com

• ynon@ynonperek.com

top related