silvio.montanari@thoughtworks.com
Agenda                           ns:              eb applicatio               MVC frameworSingle page w                   ...
Javascript:a first class citizen?
Runs in the browser                    Breaks in the browserThe language of choice                     The only choice ?Si...
The Challengeso  UI interactionso  Asynchronous communicationo  Frequent DOM manipulationHow to separate view and        H...
The MVC framework jungle                    http://todomvc.com/
A demo application
Two-way data binding     DirectivesDependency injection
Two-way data binding                                             Declarative                                              ...
DOM decoupling and behaviourview separation     Example: we want to display a     message box in alert style
Create a dom element to   represent the alert message            ...            <div class="alert_msg modal hide">        ...
<div class="alert_msg modal hide">                 data-jb-alert-box class="alert_msg modal hide">             <div class=...
DOM decoupling and behaviourview separation (2)     Example: we want to display an     overlay message while the data     ...
MainController: function(scope, repository) {!...!                                                   show the overlay when...
MainController: function(scope, repository) overlayService) {!                                  repository, {!...!  var lo...
MainController: function(scope, repository) {!MainController: function(scope, repository, overlayService) {!              ...
DOM decoupling and behaviourview separation (3)Other custom directive examples:<div data-jb-draggable>...</div>         ma...
Dependency injection  DashboardFormController: function(scope, repository) {!    scope.saveDashboard = function() {!      ...
Good practices witho  Use multiple controllers to separate the responsibilities in the different sections of your pageo  W...
Modules and namespacing  Define your module/namespacevar jashboard = (function(module) {!  module.services = angular.modul...
Organising the file structure Organise your folders      •  web-root/                                •  index.html        ...
Loading dependencies          http://javascriptmvc.com/docs.html#!stealjs          http://requirejs.org/
Loading dependencies:<script type=text/javascript !               One line in your HTML to  src=steal/steal.js?jashboard/l...
Unit testing                                     Behaviour driven development in Javascripthttp://pivotal.github.com/jasmi...
Unit testing callbacks               synchronous call          asynchronous callbackvar Controller = function(scope, http)...
Unit testing callbacks                synchronous call             asynchronous callbackvar Controller = function(scope, h...
Warning!var scope = {}, http = {};!http.getJSON = jasmine.createSpy().andReturn({!  done: function(callback) { callback("t...
Functional testing
Browser/clientside                    Asynchronous                    HTTP request                       (AJAX)  HTTP resp...
Browser/clientside                                  Asynchronous                                  HTTP request            ...
Browser/clientside                                      Asynchronous                                      HTTP request    ...
$httpBackend             (service in module ngMockE2E)http://docs.angularjs.org/api/ngMock.$httpBackendhttp://javascriptmv...
Static fixtures$.fixture("GET /ajax/dashboards","//test/.../dashboards.json");!   [       {         "id": "dashboard_1", "...
Dynamic fixtures$.fixture("GET /ajax/dashboards",function(ajaxOptions, requestSettings, headers) {!  return [200, "success...
Browser/clientside                                                 Asynchronous                                           ...
file://.../index.html?test_scenario=sample_scenario...!steal ({src: test/funcunit/test_scenario_loader.js, ignore: true});...
Example scenario                                 Static fixtures$.fixture("GET /ajax/dashboards", "//test/funcunit/fixture...
scenario_loader.js            scenario_1.js      scenario_2.js   ...   scenario_n.jsresponse_fixture_1.json     response_f...
works by overriding jQuery.ajaxTransport, basically interceptingthe jQuery.ajax() request and returning a fake responseGre...
Advanced dynamic fixtures with                            Wrapper around                            sinon.fakeServer and  ...
Simulating response delaysserver.fakeResponse("GET", "/ajax/monitor/monitor_1/runtime", {!  content: {!     last_build_tim...
Using Url templatesserver.fakeResponse("POST", //ajax/dashboard/(w+)/monitor/,    !  function(request, dashboard_id) {!   ...
Simulate scenarios not only fortesting Spike and prototype new features Explore edge cases Verify performance
Automating functional tests   o  Extension of QUnit   o  Integrated with popular automation frameworks like      Selenium ...
Examples of functional testsmodule("Feature: display monitors in a dashboard", {!  setup: function() {!     S.open(index.h...
Testing our scenarios/fixturesmodule("Feature: display monitors in a dashboard", {!  setup: function() {!     S.open(index...
Verifying expected ajax requests test("should create a new dashboard", function() {!  openDashboardDialog();!  featureHelp...
Fast functional testsWe can open the browser and run unit tests directly from the file system                    + test sc...
SUMMARYModern Javascript single page Web applications can be complex The risk introduced by such complexity should be addr...
Upcoming SlideShare
Loading in …5
×

Single page webapps & javascript-testing

6,888 views

Published on

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

Published in: Technology, Design
0 Comments
2 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
6,888
On SlideShare
0
From Embeds
0
Number of Embeds
4,402
Actions
Shares
0
Downloads
78
Comments
0
Likes
2
Embeds 0
No embeds

No notes for slide

Single page webapps & javascript-testing

  1. 1. silvio.montanari@thoughtworks.com
  2. 2. Agenda ns: eb applicatio MVC frameworSingle page w ks: s AngularJSbasic concept Javascript maturityTesting: n it n onJS, FuncUJasmine, Si A simple dem o app: Jashboa rd
  3. 3. Javascript:a first class citizen?
  4. 4. Runs in the browser Breaks in the browserThe language of choice The only choice ?Simple to test (manually) Test automation ?Lightweight and expressive MultiparadigmDynamic How do I know I made a mistake?
  5. 5. The Challengeso  UI interactionso  Asynchronous communicationo  Frequent DOM manipulationHow to separate view and How to test effectivelybehaviour Event handlingWhere’s the business logic? Data-bindingWhere’s the rendering logic? Callbacks
  6. 6. The MVC framework jungle http://todomvc.com/
  7. 7. A demo application
  8. 8. Two-way data binding DirectivesDependency injection
  9. 9. Two-way data binding Declarative Binding<input type="text" name="dashboardName” data-ng-model="dashboard.name”>...<div>{{dashboard.name}}</div> Automatic view refresh (Data) View View Model Model
  10. 10. DOM decoupling and behaviourview separation Example: we want to display a message box in alert style
  11. 11. Create a dom element to represent the alert message ... <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);!! Change the DOM Display the message
  12. 12. <div class="alert_msg modal hide"> data-jb-alert-box class="alert_msg modal hide"> <div class="modal-header"> Introduce <div>{{title}}</div> templates </div> <div class="modal-body"> <div>{{message}}</div> </div> </div> Register the alertBoxDirective: function(alertService){! element to be Wrap the return function(scope, element) {! widget logic used by the alertService.bindTo(element);! };! into a service alert service }! alertService.showAlert=function(options){!Invoke the service passing scope.title = options.title;!the message data scope.message = options.message;! $(element).modal(show);! $(".alert_msg").modal(show);!alertService.showAlert({! };! title: "Remove monitor...",! message: "If you delete..."!});!
  13. 13. DOM decoupling and behaviourview separation (2) Example: we want to display an overlay message while the data loads from the server
  14. 14. MainController: function(scope, repository) {!...! show the overlay when var loadData = function() {! loading starts $.blockUI({message: $("overlay-msg").html().trim()})! repository.loadDashboards({! repository.loadDashboards({! success: function(data) {! ! success: function(data) {! !_.each(data, function(d){scope.dashboards.push(d);});! ! });!_.each(data, function(d){scope.dashboards.push(d);}); ! !! ! };! });! ! $.unblockUI();!! };! });! hide the overlay when! };! loading completesCreate a dom element torepresent the overlay message <div class="hide"> <div class="overlay-msg"> <spam class="ajax-loader-msg"> Loading dashboards... Please wait.</spam> </div> </div>
  15. 15. MainController: function(scope, repository) overlayService) {! repository, {!...! var loadData = function() {! overlayService.show("overlay-msg");! $.blockUI({message: $("overlay-msg").html().trim()})! service Inject the repository.loadDashboards({! into the controller success: function(data) {! ! !_.each(data, function(d){scope.dashboards.push(d);});! ! function(d){scope.dashboards.push(d);}); ! ! !overlayService.hide();! !$.unblockUI();! });! };!OverlayService: function() {! var blockUISettings = { ... };! Extract the widget logic! into a service this.show = function(selector) {! blockUISettings.message = $(selector).html().trim();! $.blockUI(blockUISettings);! };! this.hide = function() {! $.unblockUI();! };!}!
  16. 16. MainController: function(scope, repository) {!MainController: function(scope, repository, overlayService) {! repository) {! Notify the view...!...! var loadData = function() {! var loadData = function() {!{ ! when data loading scope.$broadcast("DataLoadingStart");! overlayService.show("overlay-msg");! repository.loadDashboards({! starts and when it repository.loadDashboards({! repository.loadDashboards({! success: function(data) {! completes ! success: function(data) {! success: function(data) {! !_.each(data, function(d){scope.dashboards.push(d);}); !! ! });! ! !_.each(data, function(d){scope.dashboards.push(d);});! !! !_.each(data, function(d){scope.dashboards.push(d);}); ! ! ! !scope.$broadcast("DataLoadingComplete"); !! !overlayService.hide();! };! });!! };! });! });!! };! };!<div class="hide"> 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>OverlayDirective: function(scope, element, attrs) {! var actions = {! show: function(){overlayService.show(element);},! hide: function(){overlayService.hide(element);}! }! Listen to the var eventsMap = scope.$eval(attrs[jbOverlay]);! specified events _.each(_.keys(eventsMap), function(actionName) { ! scope.$on(eventsMap[actionName], actions[actionName]);! });!}!
  17. 17. DOM decoupling and behaviourview separation (3)Other custom directive examples:<div data-jb-draggable>...</div> 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
  18. 18. 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.
  19. 19. Good practices witho  Use multiple controllers to separate the responsibilities in the different sections of your pageo  Wrap your external libraries into services to provide decoupling from 3rd party pluginso  Use custom directives to define reusable componentso  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 communicateo  Isolate your objects/functions so that they can be easily tested
  20. 20. Modules and namespacing Define your module/namespacevar 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 Add functionalityjashboard = _.extend(jashboard, {! AlertService: function() {...}!});!
  21. 21. Organising the file structure Organise your folders •  web-root/ •  index.html •  lib •  jashboard One file to describe one •  controllers primary Object/Function •  directives •  model •  Dashboard.js •  plugins •  services •  AlertService.js •  HttpService.js •  test •  funcunit •  spec •  controllers •  services •  AlertServiceSpec.js •  SpecHelper.js •  SpecRunner.html
  22. 22. Loading dependencies http://javascriptmvc.com/docs.html#!stealjs http://requirejs.org/
  23. 23. Loading dependencies:<script type=text/javascript ! One line in your HTML to src=steal/steal.js?jashboard/loader.js>! dynamically load all</script>! your dependenciessteal(! { 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")!});! loader.js
  24. 24. Unit testing Behaviour driven development in Javascripthttp://pivotal.github.com/jasmine/ Advanced spying, mocking and stubbinghttp://sinonjs.org/
  25. 25. Unit testing callbacks synchronous call asynchronous callbackvar Controller = function(scope, http) {!...! this.loadData = function(){! http.getJSON("/ajax/dashboards").done(function(data) {! scope.dashboards = data;! });! };!var scope = {}, http = {};! Stub thehttp.getJSON = jasmine.createSpy().andReturn({! promise object done: function(callback) { callback("test-data"); }!}));!! verifynew Controller(scope, http).loadData();! synchronous call!expect(http.getJSON).toHaveBeenCalledWith("/ajax/dashboards");!expect(scope.dashboards).toEqual("test-data");!! verify asynchronous call
  26. 26. Unit testing callbacks synchronous call asynchronous callbackvar Controller = function(scope, http) {!...! this.loadData = function(){! http.getJSON("/ajax/dashboards").done(function(data) {! scope.dashboards = data;! });! };! Set espectations onvar scope = {}, http = {};! the synchronous callhttp.getJSON = sinon.stub();! Stub the! promise objecthttp.getJSON.withArgs("/ajax/dashboards").returns({! done: function(callback) { callback("test-data"); }!}));!!new Controller(scope, http).loadData();!! verifyexpect(scope.dashboards).toEqual("test-data");! asynchronous call!
  27. 27. 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 Highly behaviour focused testsdependencies No guaranteed objects wiring •  What if method getJSON isJavascript is a renamed?dynamic language •  What if the return value changes interface?
  28. 28. Functional testing
  29. 29. Browser/clientside Asynchronous HTTP request (AJAX) HTTP response •  HTML •  XML •  JSON Server side •  TEXT •  …
  30. 30. Browser/clientside Asynchronous HTTP request (AJAX) HTTP response •  HTML •  XML •  JSON Stub server Server side •  TEXT •  …
  31. 31. Browser/clientside Asynchronous HTTP request (AJAX) Stub HTTP response
  32. 32. $httpBackend (service in module ngMockE2E)http://docs.angularjs.org/api/ngMock.$httpBackendhttp://javascriptmvc.com/docs.html#!jQuery.fixtureFakeXMLHttpRequest!http://sinonjs.org/docs/#server
  33. 33. Static fixtures$.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
  34. 34. Dynamic fixtures$.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"! }! }]! }! ]}];!});!
  35. 35. Browser/clientside Asynchronous HTTP request (AJAX) Stub HTTP response We want the browser to use our stubbed ajax responses only during our tests, without having to change our code
  36. 36. 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
  37. 37. Example scenario Static fixtures$.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: [] } }, {} ];!});! Dynamic fixture
  38. 38. scenario_loader.js scenario_1.js scenario_2.js ... scenario_n.jsresponse_fixture_1.json response_fixture_2.json ... response_fixture_n.json
  39. 39. works by overriding jQuery.ajaxTransport, basically interceptingthe jQuery.ajax() request and returning a fake responseGreat for static fixturesIt only works with jQueryLimited support for templated UrlsSimulating a delayed response affects all the responses
  40. 40. Advanced dynamic fixtures with Wrapper around sinon.fakeServer and sinon.useFakeXMLHttpRequest var server = new jashboard.test.SinonFakeServer();! ! server.fakeResponse = function(httpMethod, url, response);! response = {! returnCode: 200,! contentType: "application/json",! content: {},! delay: 1! }!
  41. 41. Simulating response delaysserver.fakeResponse("GET", "/ajax/monitor/monitor_1/runtime", {! content: {! last_build_time: "23-08-2012 14:32:23",! duration: 752,! success: true,! status: 1! },! we can set individual response delay: 3! delay time for each response});!!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!});!
  42. 42. Using Url templatesserver.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};!});!
  43. 43. Simulate scenarios not only fortesting Spike and prototype new features Explore edge cases Verify performance
  44. 44. 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
  45. 45. Examples of functional testsmodule("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"); ! });!});!
  46. 46. Testing our scenarios/fixturesmodule("Feature: display monitors in a dashboard", {! setup: function() {! S.open(index.html?test_scenario=display_dashboards_data);! 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 ..."! }! );!});!
  47. 47. Verifying expected ajax requests test("should create a new dashboard", function() {! openDashboardDialog();! featureHelper.inputText("input[name=dashboardName]", "TEST");! S("#saveDashboard").visible().click();! ...! funcunit test$.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 scenario
  48. 48. Fast functional testsWe can open the browser and run unit tests directly from the file system + test scenarios + response fixtures
  49. 49. SUMMARYModern Javascript single page Web applications can be complex 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 refactoringLibraries like $.fixture and Sinon.JS can be helpful for rapidspiking/prototyping and testing of front-end features

×