Upload
smontanari
View
5.759
Download
2
Tags:
Embed Size (px)
DESCRIPTION
My presentation at the Edge of the Web conference, extended with a few more slides and examples
Citation preview
Agenda
Single page web applications:
basic concepts MVC frameworks: AngularJS
Javascript maturity
Testing:
Jasmine, SinonJS, FuncUnit
A simple demo app: Jashboard
Javascript: a first class citizen?
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?
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?
http://todomvc.com/
The MVC framework jungle
A demo application
Two-way data binding
Directives
Dependency injection
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
DOM decoupling and behaviour view separation
Example: we want to display a message box in alert style
... <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
<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
DOM decoupling and behaviour view separation (2)
Example: we want to display an overlay message while the data loads from the server
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
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
<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
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
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.
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
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
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
Loading dependencies
http://javascriptmvc.com/docs.html#!stealjs
http://requirejs.org/
<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
Unit testing
Behaviour driven development in Javascript
Advanced spying, mocking and stubbing
http://sinonjs.org/
http://pivotal.github.com/jasmine/
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
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
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?
Functional testing
Server side
Asynchronous HTTP request
(AJAX)
HTTP response • HTML • XML • JSON • TEXT • …
Browser/client side
Server side
Asynchronous HTTP request
(AJAX)
HTTP response • HTML • XML • JSON • TEXT • …
Browser/client side
Stub server
Asynchronous HTTP request
(AJAX)
Browser/client side
Stub HTTP response
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
$.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
$.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
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
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
$.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
scenario_loader.js
scenario_1.js scenario_2.js scenario_n.js ...
response_fixture_1.json response_fixture_2.json response_fixture_n.json ...
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
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!}!
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
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
Simulate scenarios not only for testing
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 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
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
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
$.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
We can open the browser and run unit tests directly from the file system
+ test scenarios + response fixtures
Fast functional tests
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