reliable javascript
TRANSCRIPT
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.
▸ 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)
1.NOT IN THE TERMINAL▸ The client side feels 'separate' from our usual tool chain.
▸ Doesn't integrate with other CI tools.
3. APIS▸ Client-side applications are rarely self contained.
▸ Still dependant on Server-Side and 3rd party applications
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.
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.
spec/ReverseSpec.jsdescribe('strReverse function', function() {
it('should return the inverse of a string', function() { var result = strReverse('hello'); expect(result).toBe('olleh'); });
});
"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."
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();});
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';
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();});
/src/Post.js$(document).ready(function() { $("body").on("click", ".post > .read-more", function(e) { $(this).siblings("article").show(); });});
“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
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.
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(); }); });});
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");});
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();});
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 });});
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