reliable javascript

76
BUILDING RELIABLE WEB APPS A Guide to Client Side Testing GLENN STOVALL

Upload: glenn-stovall

Post on 18-Jul-2015

185 views

Category:

Technology


0 download

TRANSCRIPT

BUILDING RELIABLE WEB APPS

A Guide to Client Side Testing

GLENN STOVALL

MOVING TO THE CLIENT SIDEMore applications are moving more code from the server side to the

client side.

▸ Greater decoupling of rendering & business logic.▸ More responsive UX.

▸ More Devices = More API-centric approach.

JAVASCRIPT IS A 1ST CLASS CITIZENThis means we should treat it the same way we treat our server side

languages, and that means it needs to be tested.

WHO AM I?AND WHY SHOULD YOU CARE?

▸ Glenn Stovall▸ Technical Consultant

▸ Have worked on large scale front end applications.▸ Worked with multiple tech companies to improve their internal

practices.

WHAT THIS TALK IS

▸ Overview of the challenges we face. ▸ Tools and techniques to overcome them. ▸ As platform agnostic as possible

▸ 3 Examples using the user story > test > code cycle (BDD)

4 CHALLENGESOF FRONT END TESTING

a.k.a excuses

1.NOT IN THE TERMINAL▸ The client side feels 'separate' from our usual tool chain.

▸ Doesn't integrate with other CI tools.

2. THE DOM▸ Difficult to simulate browser behavior.

3. APIS▸ Client-side applications are rarely self contained.

▸ Still dependant on Server-Side and 3rd party applications

4. ASYNC▸ How can you test code when you don't know when it's going to be

done?

OUR 3 STORIES1. Testing a simple string manipulation function.

2. Testing a "read more" button (DOM Manipulation).3. Testing code reliant on a 3rd party API.

THE TOOLS

1. NODEJSSERVER SIDE JAVASCRIPT.

2. GRUNTJAVASCRIPT TASK RUNNER.

3. JASMINEJAVASCRIPT TESTING FRAMEWORK.

4. JQUERYDOM MANIPULATION, SIMULATION, AND

SELECTION.

5. PHANTOMJSBROWSER SIMULATION.

SETTING UP OUR ENVIRONMENT

STEP 0: INSTALL NODEJShttp://nodejs.org/

download/

STEP 1: DOWNLOAD JASMINE STANDALONE▸ https://github.com/jasmine/jasmine/tree/

master/dist

▸ Can open SpecRunner.html in a browser to see tests.▸ Remove example tests if you want.

STEP 2: INSTALL GRUNT + GRUNT-JASMINEFrom the project root directory:▸ npm install grunt

▸ npm install grunt-jasmine

STEP 3: WRITE YOUR GRUNTFILE.JSmodule.exports = function(grunt) { grunt.initConfig({ jasmine : { src : 'src/*.js', options: { specs : 'spec/*Spec.js', helpers: 'spec/*Helper.js' } } });

grunt.loadNpmTasks('grunt-contrib-jasmine'); };

You can now run your front end tests on the back end by calling grunt jasmine.

And we are done with setting up our environment.

FEATURE #1

STRING REVERSE FUNCTION

"As a developer, I should be able to reverse a string, so that I can learn

about testing"

spec/ReverseSpec.jsdescribe('strReverse function', function() {

it('should return the inverse of a string', function() { var result = strReverse('hello'); expect(result).toBe('olleh'); });

});

FAIL

/src/Util.jsfunction strReverse(str) { return str.split("").reverse().join("");}

PASS

FEATURE #2

READ MORE BUTTON

"As a user, I should be able to click a button labeled “read more” in order to view the content of an article, so I can

read it."

BUT FIRST...

JASMINE-JQUERY▸ Add on library that gives us additional tools for testing HTML & CSS

related functionality.▸ Download this file and place it in /vendor directory.▸ We'll load jQuery from a CDN (because we can)

Gruntfile.jsgrunt.initConfig({ jasmine: ... vendor: [ "http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js", "https://raw.githubusercontent.com/velesin/jasmine-jquery/master/lib/jasmine-jquery.js" ], ...});

Vendor scripts are loaded first.

FIXTURES▸ HTML Template files you can use in your tests.

▸ Add this line to spec/SpecHelper.js and create the directory:

jasmine.getFixtures().fixturesPath = 'spec/fixtures/html';

spec/fixtures/html/post.html<section class='post'> This is a summary. <button class='read-more'>read more</button> <article>This is the full article.</article></section>

SET UP + TEAR DOWN▸ beforeAll() : runs once at the beginning of the suite.

▸ beforeEach(): runs before every test.▸ afterEach(): runs after every test.

▸ afterAll() : runs once at the end of the suite.

/spec/PostSpec.jsdescribe('article', function() { var content = null;

beforeEach(function() { content = $(readFixtures("post.html")); $('body').append(content); });

afterEach(function() { $('body').remove(".post"); });});

/spec/PostSpec.jsit('should not display the content by default', function() { expect(content.find("article")).toBeHidden();});

FAIL

STYLE FIXTURES▸ CSS files you can use in your tests.

▸ Add this line to spec/SpecHelper.js and create the directory:

jasmine.getStyleFixtures().fixturesPath = 'spec/fixtures/css';

/spec/fixtures/css/post.css.post > article { display: none;}

/spec/PostSpec.js beforeEach(function() { loadStyleFixtures("post.css"); ... });

PASS

TESTING THE 'READ MORE' BUTTON/spec/PostSpec.js

it('should display the article when you click "read more"', function() { content.find(".read-more").click(); expect(content.find("article")).toBeVisible();});

FAIL

/src/Post.js$(document).ready(function() { $("body").on("click", ".post > .read-more", function(e) { $(this).siblings("article").show(); });});

PASS

FEATURE #3

REDDIT API + AJAX

“As a user, I would like to see the cutest animal of the month according to

Reddit, so that I can bring some joy into an other wise listless and dreary

existance."

PRO TIPYou can change any URL on Reddit to an API call by adding .json to

the end of the URL.http://www.reddit.com/r/aww/top.json?sort=top&t=month

BUT FIRST...

JSON FIXTURES▸ JSON Files you can use in your tests.

▸ Add this line to spec/SpecHelper.js and create the directory:

jasmine.getJSONFixtures().fixturesPath = 'spec/fixtures/json';`

/spec/fixtures/json/aww.json{ "kind" : "listing", "data" : { "children" : [ { "data" : { "title" : "Our indoor cat moved from a gray apartment block view to this", "url" : "http://i.imgur.com/3rYHhEu.jpg" } } ] }}

SPIES▸ Can track when functions are called.▸ Can be called before or After functions.

▸ Can be called instead of functions and return values.

We can use spies to mock Service objects, so that we can test other code that relies on these objects without being dependant on them.

src/AwwService.jsvar AwwService = {}; AwwService.query = function() { return null;}

spec/AwwServiceSpec.jsbeforeEach(function() { spyOn(AwwService, 'query').and.callFake( function(params) { return getJSONFixture('aww.json'); });});

PROBLEMS WITH THIS APPROACH▸ Functions can't return values from async calls

▸ We could use async: false, but this approach is slow.▸ Instead, query() should take a callback, and we can test against

that.

src/AwwService.js AwwService.query = function(callback) { $.ajax({ url: "http://www.reddit.com/r/aww/top.json", data : { "sort" : "top", "t" : "month" }, success: callback }); }

TIMING AJAX CALLS▸ beforeEach() and it() have an optional done paramater.

▸ Tests will not run until done() is called. ▸ By adding done() to our callbacks, we can test async behavior

src/AwwSerivce.jsAwwService.displayTopResult = function(listings) { var img = $("<img>").attr("src",listings.data.children[0].data.url); $("body").append(img);}

By placing the logic in a separate function we achieve the following:▸ Test this functionality on its own (using our JSON fixture).

▸ Use this callback in the app itself.▸ Create our own callback for testing, which will call done().

spec/AwwService.js describe('AwwService', function() { describe('query Function', function() {

beforeEach(function(done) { AwwService.query(function(results) { AwwService.displayTopResult(results); done(); }); });

afterEach(function(done) { $("body").remove("img"); done(); }); });});

NOW WE CAN WRITE OUR FIRST TEST (FINALLY).

spec/AwwService.jsit('should run the callback provided', function() { var imgs = $("body").find("img"); var firstImg = img.first(); expect(imgs.length).toBe(1); expect(firstImg.attr("src")).toEqual("http://i.imgur.com/3rYHhEu.jpg");});

PASSBUT...

PROBLEMS WITH THIS APPROACH▸ This test is dependant on the Reddit API.

▸ The first assertion will fail if the API is ever unavailable.▸ The second assertion will fail if the result changes.▸ We need to mock the result of HTTP request.

JASMINE-AJAX▸ Jasmine provides a library that can intercept calls.

▸ Allows us to control when they are called, and how they respond.▸ Need to download the file, add it to our vendor directory.▸ Let's add this to our test, and create a mock response.

spec/AwwServiceSpec.jsbeforeEach(function() { jasmine.Ajax.install();});

afterEach(function() { jasmine.Ajax.uninstall();});

FAIL

spec/AwwSerivce.jsbeforeEach(function() { // ...our original AJAX call... var responseText = JSON.stringify({ ... });

jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, 'content/type': 'application/javascript', 'responseText': responseText });});

PASS

PROBLEM WITH THIS APPROACH▸ jasmine-jquery uses AJAX calls to load fixtures.▸ jasmine-ajax intercepts and breaks these calls.▸ You can use preloadFixtures() before the

jasmine.Ajax.install() call to load HTML fixtures into the cache.

▸ There is currently no preloading for CSS/JSON.

CONCLUSION▸ This should be more than enough to get you started on client side

testing. ▸ Any tests are better than no tests.

▸ Client side applications aren't going to get any less complicated.

FURTHER INFORMATION▸ http://glennstovall.com

[email protected]▸ @GSto

- LINK TO SHOW NOTESANY QUESTIONS