single page webapps & javascript-testing

49

Upload: smontanari

Post on 17-May-2015

5.758 views

Category:

Technology


2 download

DESCRIPTION

My presentation at the Edge of the Web conference, extended with a few more slides and examples

TRANSCRIPT

Page 2: Single page webapps & javascript-testing

Agenda

Single page web applications:

basic concepts MVC frameworks: AngularJS

Javascript maturity

Testing:

Jasmine, SinonJS, FuncUnit

A simple demo app: Jashboard

Page 3: Single page webapps & javascript-testing

Javascript: a first class citizen?

Page 4: Single page webapps & javascript-testing

Runs in the browser Breaks in the browser

The language of choice

Simple to test (manually)

Lightweight and expressive

Dynamic

The only choice ?

Test automation ?

Multiparadigm

How do I know I made a mistake?

Page 5: Single page webapps & javascript-testing

The Challenges

How to test effectively Event handling

Data-binding

Callbacks

o UI interactions

o Asynchronous communication

o  Frequent DOM manipulation

How to separate view and behaviour Where’s the business logic?

Where’s the rendering logic?

Page 6: Single page webapps & javascript-testing

http://todomvc.com/

The MVC framework jungle

Page 7: Single page webapps & javascript-testing

A demo application

Page 8: Single page webapps & javascript-testing

Two-way data binding

Directives

Dependency injection

Page 9: Single page webapps & javascript-testing

Two-way data binding

<input type="text" name="dashboardName” data-ng-model="dashboard.name”> ... <div>{{dashboard.name}}</div>

(Data) Model View View

Model

Declarative Binding

Automatic view refresh

Page 10: Single page webapps & javascript-testing

DOM decoupling and behaviour view separation

Example: we want to display a message box in alert style

Page 11: Single page webapps & javascript-testing

... <div class="alert_msg modal hide"> <div class="modal-header"> </div> <div class="modal-body"> </div> </div> ...

!$(".alert_msg .modal-header").html('<div>Remove monitor ...</div>');!$(".alert_msg .modal-body").html('<div>If you delete...</div>');!$(".alert_msg").modal('show');!!

Create a dom element to represent the alert message

Change the DOM Display the message

Page 12: Single page webapps & javascript-testing

<div class="alert_msg modal hide"> <div class="modal-header">

<div>{{title}}</div> </div> <div class="modal-body"> <div>{{message}}</div> </div> </div>

<div data-jb-alert-box class="alert_msg modal hide"> <div class="modal-header">

<div>{{title}}</div> </div> <div class="modal-body"> <div>{{message}}</div> </div> </div>

alertService.showAlert=function(options){! scope.title = options.title;! scope.message = options.message;! $(".alert_msg").modal('show');!};!

alertService.showAlert=function(options){! scope.title = options.title;! scope.message = options.message;! $(element).modal('show');!};!alertService.showAlert({!

title: "Remove monitor...",! message: "If you delete..."!});!

Invoke the service passing the message data

Introduce templates

Wrap the widget logic into a service

alertBoxDirective: function(alertService){! return function(scope, element) {! alertService.bindTo(element);! };!}!

Register the element to be used by the alert service

Page 13: Single page webapps & javascript-testing

DOM decoupling and behaviour view separation (2)

Example: we want to display an overlay message while the data loads from the server

Page 14: Single page webapps & javascript-testing

MainController: function(scope, repository) {!...! var loadData = function() {! repository.loadDashboards({! success: function(data) {!

! !_.each(data, function(d){scope.dashboards.push(d);});! });! };!!!

MainController: function(scope, repository) {!...! var loadData = function() {! $.blockUI({message: $("overlay-msg").html().trim()})! repository.loadDashboards({! success: function(data) {!

! _.each(data, function(d){scope.dashboards.push(d);}); ! !! });! };!!

<div class="hide"> <div class="overlay-msg"> <spam class="ajax-loader-msg"> Loading dashboards... Please wait.</spam> </div> </div>

Create a dom element to represent the overlay message

MainController: function(scope, repository) {!...! var loadData = function() {! $.blockUI({message: $("overlay-msg").html().trim()})! repository.loadDashboards({! success: function(data) {!

! _.each(data, function(d){scope.dashboards.push(d);}); ! !! $.unblockUI();!

});! };!

show the overlay when loading starts

hide the overlay when loading completes

Page 15: Single page webapps & javascript-testing

MainController: function(scope, repository) {!...! var loadData = function() {! $.blockUI({message: $("overlay-msg").html().trim()})! repository.loadDashboards({! success: function(data) {!

! !_.each(data, function(d){scope.dashboards.push(d);});!! !$.unblockUI();!

});! };!

MainController: function(scope, repository, overlayService) {!...! var loadData = function() {! overlayService.show("overlay-msg");! repository.loadDashboards({! success: function(data) {!

! !_.each(data, function(d){scope.dashboards.push(d);}); ! ! ! !overlayService.hide();!

});! };!

OverlayService: function() {! var blockUISettings = { ... };!! this.show = function(selector) {! blockUISettings.message = $(selector).html().trim();! $.blockUI(blockUISettings);! };! this.hide = function() {! $.unblockUI();! };!}!

Extract the widget logic into a service

Inject the service into the controller

Page 16: Single page webapps & javascript-testing

<div class="hide"> <div class="overlay-msg"> <spam class="ajax-loader-msg"> Loading dashboards... Please wait.</spam> </div> </div>

<div class="hide" data-jb-overlay="{show:'DataLoadingStart’,hide:'DataLoadingComplete'}"> <div class="overlay-msg"> <spam class="ajax-loader-msg"> Loading dashboards... Please wait.</spam> </div> </div>

MainController: function(scope, repository, overlayService) {!...! var loadData = function() {! overlayService.show("overlay-msg");! repository.loadDashboards({! success: function(data) {!

! !_.each(data, function(d){scope.dashboards.push(d);}); ! ! !overlayService.hide();! });! };!

MainController: function(scope, repository) {!...! var loadData = function() { ! repository.loadDashboards({! success: function(data) {!

! !_.each(data, function(d){scope.dashboards.push(d);}); !! });! };!!!

MainController: function(scope, repository) {!...! var loadData = function() {! scope.$broadcast("DataLoadingStart");! repository.loadDashboards({! success: function(data) {!

! !_.each(data, function(d){scope.dashboards.push(d);}); !! });! };!!

MainController: function(scope, repository) {!...! var loadData = function() {! scope.$broadcast("DataLoadingStart");! repository.loadDashboards({! success: function(data) {!

! !_.each(data, function(d){scope.dashboards.push(d);});!! !scope.$broadcast("DataLoadingComplete");!!

});! };!

Notify the view when data loading starts and when it completes

OverlayDirective: function(scope, element, attrs) {! var actions = {! show: function(){overlayService.show(element);},! hide: function(){overlayService.hide(element);}! }! var eventsMap = scope.$eval(attrs['jbOverlay']);! _.each(_.keys(eventsMap), function(actionName) { ! scope.$on(eventsMap[actionName], actions[actionName]);! });!}!

Listen to the specified events

Page 17: Single page webapps & javascript-testing

DOM decoupling and behaviour view separation (3)

<div data-jb-draggable>...</div>

Other custom directive examples:

make an element draggable

<div data-jb-resizable>...</div> make an element resizable

<div data-jb-tooltip>...</div> set an element tooltip

<div data-jb-dialog>...</div> open an element in a dialog

<div data-jb-form-validation>...</div> trigger input validation rules

Page 18: Single page webapps & javascript-testing

Dependency injection DashboardFormController: function(scope, repository) {! scope.saveDashboard = function() {! repository.createDashboard({name: this.dashboardName});! ...!!

Repository: function(httpService) {! this.createDashboard = function(parameters) {!

!httpService.postJSON("/ajax/dashboard", parameters);! };! ...!!

DashboardFormController Repository HttpService

With Angular you can use plain javascript to define your models, services, controllers, etc.

Page 19: Single page webapps & javascript-testing

o  Use multiple controllers to separate the responsibilities in the different sections of your page o  Wrap your external libraries into services to provide

decoupling from 3rd party plugins o  Use custom directives to define reusable components o  The primary function of the Angular Scope is to be

the execution context (model) for your views/templates. Be mindful when leveraging scope inheritance and scope data sharing. o  Use events as means to communicate o  Isolate your objects/functions so that they can be

easily tested

Good practices with

Page 20: Single page webapps & javascript-testing

Modules and namespacing

var jashboard = (function(module) {! module.services = angular.module('jashboard.services', []);! module.application = angular.module('jashboard',...); ! return module;!}(jashboard || {}));!

http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth

jashboard = _.extend(jashboard, {! AlertService: function() {...}!});!

Define your module/namespace

Add functionality

Page 21: Single page webapps & javascript-testing

Organising the file structure

One file to describe one primary Object/Function

Organise your folders •  web-root/ •  index.html •  lib •  jashboard

•  controllers •  directives •  model

•  Dashboard.js •  plugins •  services

•  AlertService.js •  HttpService.js

•  test •  funcunit •  spec

•  controllers •  services

•  AlertServiceSpec.js •  SpecHelper.js

•  SpecRunner.html

Page 22: Single page webapps & javascript-testing

Loading dependencies

http://javascriptmvc.com/docs.html#!stealjs

http://requirejs.org/

Page 23: Single page webapps & javascript-testing

<script type='text/javascript' ! src='steal/steal.js?jashboard/loader.js'>!</script>!

steal(! { src: "css/bootstrap.min.css", packaged: false },! ...!).then(! { src: 'lib/angular.min.js', packaged: false },! { src: 'lib/underscore-min.js', packaged: false },! { src: 'lib/bootstrap.min.js', packaged: false },! ...!).then(function() {! steal('steal/less')! .then("css/jashboard.less")! .then("jashboard/modules.js")!});!

One line in your HTML to dynamically load all your dependencies

Loading dependencies:

loader.js

Page 24: Single page webapps & javascript-testing

Unit testing

Behaviour driven development in Javascript

Advanced spying, mocking and stubbing

http://sinonjs.org/

http://pivotal.github.com/jasmine/

Page 25: Single page webapps & javascript-testing

var Controller = function(scope, http) {!...! this.loadData = function(){! http.getJSON("/ajax/dashboards").done(function(data) {! scope.dashboards = data;! });! };!

synchronous call

Unit testing callbacks

var scope = {}, http = {};!http.getJSON = jasmine.createSpy().andReturn({! done: function(callback) { callback("test-data"); }!}));!!new Controller(scope, http).loadData();!!expect(http.getJSON).toHaveBeenCalledWith("/ajax/dashboards");!expect(scope.dashboards).toEqual("test-data");!!

asynchronous callback

Stub the promise object

verify synchronous call

verify asynchronous call

Page 26: Single page webapps & javascript-testing

var Controller = function(scope, http) {!...! this.loadData = function(){! http.getJSON("/ajax/dashboards").done(function(data) {! scope.dashboards = data;! });! };!

synchronous call

Unit testing callbacks

var scope = {}, http = {};!http.getJSON = sinon.stub();!!http.getJSON.withArgs("/ajax/dashboards").returns({! done: function(callback) { callback("test-data"); }!}));!!new Controller(scope, http).loadData();!!expect(scope.dashboards).toEqual("test-data");!!

asynchronous callback

Set espectations on the synchronous call

verify asynchronous call

Stub the promise object

Page 27: Single page webapps & javascript-testing

Warning! var scope = {}, http = {};!http.getJSON = jasmine.createSpy().andReturn({! done: function(callback) { callback("test-data"); }!}));!!new Controller(scope, http).loadData();!!expect(http.getJSON).toHaveBeenCalledWith("/ajax/dashboards");!expect(scope.dashboards).toEqual("test-data");!!

Mocking and stubbing dependencies

Javascript is a dynamic language

Highly behaviour focused tests

No guaranteed objects wiring •  What if method getJSON is

renamed? •  What if the return value changes

interface?

Page 28: Single page webapps & javascript-testing

Functional testing

Page 29: Single page webapps & javascript-testing

Server side

Asynchronous HTTP request

(AJAX)

HTTP response •  HTML •  XML •  JSON •  TEXT •  …

Browser/client side

Page 30: Single page webapps & javascript-testing

Server side

Asynchronous HTTP request

(AJAX)

HTTP response •  HTML •  XML •  JSON •  TEXT •  …

Browser/client side

Stub server

Page 31: Single page webapps & javascript-testing

Asynchronous HTTP request

(AJAX)

Browser/client side

Stub HTTP response

Page 32: Single page webapps & javascript-testing

http://javascriptmvc.com/docs.html#!jQuery.fixture

FakeXMLHttpRequest!

$httpBackend (service in module ngMockE2E)

http://docs.angularjs.org/api/ngMock.$httpBackend

http://sinonjs.org/docs/#server

Page 33: Single page webapps & javascript-testing

$.fixture("GET /ajax/dashboards","//test/.../dashboards.json");!

[ { "id": "dashboard_1", "name": "first dashboard", "monitors": [ { "id": "monitor_1", "name": "Zombie-Dash build", "refresh_interval": 10, "type": "build", "configuration": { "type": "jenkins", "hostname": "zombie-dev.host.com", "port": 9080, "build_id": "zombie_build" } } ] }, { "id": "dashboard_2", "name": "second dashboard”, "monitors": [] } ]

dashboards.json

Static fixtures

Page 34: Single page webapps & javascript-testing

$.fixture("GET /ajax/dashboards", function(ajaxOptions, requestSettings, headers) {! return [200, "success", {json: [! {! id: "dashboard_1", name: "my dashboard",! monitors: [! {! id: "monitor_1",! name: "Zombie-Dash build",! refresh_interval: 10,! type: "build",! configuration: {! type: "jenkins",! hostname: "zombie-dev.host.com",! port: 9080,! build_id: "zombie_build"! }! }]! }! ]}];!});!

Dynamic fixtures

Page 35: Single page webapps & javascript-testing

Asynchronous HTTP request

(AJAX)

Browser/client side

Stub HTTP response

We want the browser to use our stubbed ajax responses only during our tests, without having to change our code

Page 36: Single page webapps & javascript-testing

file://.../index.html?test_scenario=sample_scenario

...!steal ({src: 'test/funcunit/test_scenario_loader.js', ignore: true});!!

(function() {! var regexp = /\?test_scenario=(\w+)/! var match = regexp.exec(window.location.search);! if (match) {! var scenarioName = match[1];! steal(! { src: 'lib/sinon-1.5.2.js', ignore: true },! { src: 'jquery/dom/fixture', ignore: true }! ).then("test/funcunit/scenarios/" + scenarioName + ".js");! }!}());!

test_scenario_loader.js

Page 37: Single page webapps & javascript-testing

$.fixture("GET /ajax/dashboards", "//test/funcunit/fixtures/fixture_dashboards.json");!$.fixture("GET /ajax/monitor/monitor_1/runtime", "//test/funcunit/fixtures/fixture_build_monitor_1.json");!!$.fixture("POST /ajax/dashboard", function(ajaxOriginalOptions, !ajaxOptions, headers) {! var data = JSON.parse(ajaxOptions.data);!! return [201, "success", {json: {id: "dashboard_4", name: ! data.name, monitors: [] } }, {} ];!});!

Example scenario

Dynamic fixture

Static fixtures

Page 38: Single page webapps & javascript-testing

scenario_loader.js

scenario_1.js scenario_2.js scenario_n.js ...

response_fixture_1.json response_fixture_2.json response_fixture_n.json ...

Page 39: Single page webapps & javascript-testing

works by overriding jQuery.ajaxTransport, basically intercepting the jQuery.ajax() request and returning a fake response

It only works with jQuery

Limited support for templated Urls

Simulating a delayed response affects all the responses

Great for static fixtures

Page 40: Single page webapps & javascript-testing

Advanced dynamic fixtures with

var server = new jashboard.test.SinonFakeServer();!!server.fakeResponse = function(httpMethod, url, response);!

Wrapper around sinon.fakeServer and sinon.useFakeXMLHttpRequest

response = {! returnCode: 200,! contentType: "application/json",! content: {},! delay: 1!}!

Page 41: Single page webapps & javascript-testing

server.fakeResponse("GET", "/ajax/monitor/monitor_1/runtime", {! content: {! last_build_time: "23-08-2012 14:32:23",! duration: 752,! success: true,! status: 1! },! delay: 3!});!!server.fakeResponse("GET", "/ajax/monitor/monitor_2/runtime", {! content: {! last_build_time: "25-08-2012 15:56:45",! duration: 126,! success: false,! status: 0! },! delay: 1!});!

Simulating response delays

we can set individual response delay time for each response we can set individual response delay time for each response

Page 42: Single page webapps & javascript-testing

server.fakeResponse("POST", /\/ajax\/dashboard\/(\w+)\/monitor/, ! function(request, dashboard_id) {!

!...!});!!server.fakeResponse("PUT", /\/ajax\/monitor\/(\w+)\/position/, ! function(request, monitor_id) {! var position = JSON.parse(request.requestBody);! console.log(monitor_id + " moved to [" + position.top + ”, " + position.left + "]");! return {returnCode: 201};!});!

Using Url templates

Page 43: Single page webapps & javascript-testing

Simulate scenarios not only for testing

Spike and prototype new features

Explore edge cases

Verify performance

Page 44: Single page webapps & javascript-testing

Automating functional tests

o  Extension of QUnit o  Integrated with popular automation frameworks like

Selenium and PhantomJS (?) •  Open a web page •  Use a jQuery-like syntax to look up elements and simulate

a user action •  Wait for a condition to be true •  Run assertions

Page 45: Single page webapps & javascript-testing

module("Feature: display monitors in a dashboard", {! setup: function() {! S.open('index.html');! }!});!test("should load and display build monitor data", function() {! S("#tab-dashboard_2").visible().click();! S("#monitor_2 .monitor-title").visible().text("Epic build");! ...!)}!!module("Feature: create a new dashboard", {!...!test("should create a new dashboard", function() {! //open form dialog! ...! S("input[name='dashboardName']).visible().type("some name");! S("#saveDashboard").visible().click();! S(".dashboard-tab").size(4, function() {! equal(S(".dashboard-tab").last().text(), "some name"); ! });!});!

Examples of functional tests

Page 46: Single page webapps & javascript-testing

module("Feature: display monitors in a dashboard", {! setup: function() {! S.open('index.html');! }!});!test("should load and display build monitor data", function() {! S("#tab-dashboard_2").visible().click();! featureHelper.verifyElementContent("#monitor_2",! {! '.monitor-title': "Epic build",! '.build-time': "28-08-2012 11:25:10",! '.build-duration': "09:56",! '.build-result': "failure",! '.build-status': "building"! }! );! featureHelper.verifyElementContent("#monitor_3",! {! '.monitor-title': "Random text",! 'pre': "some very random generated text ..."! }! );!});!

module("Feature: display monitors in a dashboard", {! setup: function() {! S.open('index.html?test_scenario=display_dashboards_data');! }!});!test("should load and display build monitor data", function() {! S("#tab-dashboard_2").visible().click();! featureHelper.verifyElementContent("#monitor_2",! {! '.monitor-title': "Epic build",! '.build-time': "28-08-2012 11:25:10",! '.build-duration': "09:56",! '.build-result': "failure",! '.build-status': "building"! }! );! featureHelper.verifyElementContent("#monitor_3",! {! '.monitor-title': "Random text",! 'pre': "some very random generated text ..."! }! );!});!

Testing our scenarios/fixtures

Page 47: Single page webapps & javascript-testing

$.fixture("POST /ajax/dashboard", function(ajaxOriginalOptions, ! ajaxOptions, headers) {! var data = JSON.parse(ajaxOptions.data);!! if("TEST" === data.name) {! return [201, "success", {json: {id: "dashboard_4", name: "TEST", ! monitors: [] } }, {} ];! }! throw "unexpected data in the POST request: " + ajaxOptions.data;!});!

test("should create a new dashboard", function() {! openDashboardDialog();! featureHelper.inputText("input[name='dashboardName']", "TEST");! S("#saveDashboard").visible().click();! ...!

Verifying expected ajax requests

funcunit test

test scenario

Page 48: Single page webapps & javascript-testing

We can open the browser and run unit tests directly from the file system

+ test scenarios + response fixtures

Fast functional tests

Page 49: Single page webapps & javascript-testing

The risk introduced by such complexity should be addressed by adopting proper practices, such as

o  leveraging frameworks that can simplify the development

o  keeping a neat and organised project code structure

o  applying rules of simple design to create readable and maintainable codebase

o  using mocks / stubs to create concise unit tests

o  running fast functional regression tests to increase confidence in refactoring

SUMMARY Modern Javascript single page Web applications can be complex

Libraries like $.fixture and Sinon.JS can be helpful for rapid spiking/prototyping and testing of front-end features