how to write easy-to-test javascript
Post on 18-Dec-2014
572 Views
Preview:
DESCRIPTION
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