Frontend Testing


with Mocha

http://seattlejs.training.formidablelabs.com

@FormidableLabs | formidablelabs.com

Tip - space bar advances slides

Formidable Labs

Formidable Labs

Frontend testing

Automated tests that run your client-side code, interact with it, and then compare the resulting state against expected state.

The Motivation

Business logic and sophisticated UX have moved into the browser.

The Challenges

Frontend testing is difficult and error-prone.

  • Asynchronous events, timing
  • Browser idiosyncrasies
  • State of testing technologies

However

... the tools and knowledge are really catching up fast and continually evolving.

Backbone.js Testing

Backbone.js Testing

Overview

  • Setup
  • Test Suites
  • Assertions
  • Fakes
  • Running it

Setup

Our Stack

Directory layout

MY_APP_NAME/
  app/
    index.html
    js/
  test/
    test.html
    js/
      main-browser.js
      spec/
        deps.js
        *.spec.js
        

test.html

<script src="/PATH/mocha/mocha.js"></script>
<script src="/PATH/chai/chai.js"></script>
<script src="/PATH/sinonjs/sinon.js"></script>
<script>
  // Set up Chai and Mocha.
  window.expect = chai.expect;
  mocha.setup("bdd");
  
  // Run tests on window load.
  window.onload = function () {
    mocha.run();
  };
</script>

Production Worthy

From an example app.

The Test Reporter

Here's what test.html looks like in the browser

Notice that we have clickable headings and tests, and ways to execute single tests.

Mocha Suites, Specs

  • Spec: A test.
  • Suite: A collection of specs or suites.

Suites, Specs (API)

  • Suites: describe
  • Specs: it
  • Control: skip, only
  • Async: done

Pending Spec Demo


                describe("single level", function () {
                  it("should test something");
                });
                

Basic Spec Demo


                it("tests my basic assumptions", function () {
                  if (2 + 2 != 4) {
                    throw "reconsider everything";
                  }
                });
                

Skipped Spec Demo


                describe("tdd tests", function () {
                
                  // take out 'skip' to watch it fail
                  it.skip("tests something low priority", function () {
                    throw "implement me"
                  });
                  
                  it("tests the thing i finished", function () {
                    // redacted
                  });
                  
                });
                

Only Spec Demo


                describe("tdd tests", function () {
                
                  it("tests something i already did", function () {
                    // redacted
                  });
                  
                  // use 'only' to run one test
                  it.only("tests the thing i'm working on", function () {
                    // redacted
                  });
                  
                });
                

Async Spec Demo


                it("is slow and async", function (done) {
                  setTimeout(function () { done(); }, 300);
                });
                

Nested Suites Demo


                describe("top-level", function () {
                  describe("nested", function () {
                    it("should test something");
                  });
                });
                

Setup/Teardown

  • before, beforeEach
  • after, afterEach

Setup/Teardown Demo


                describe("My Suite", function () {
                  // [TRY] `after`, `before`
                  beforeEach(function () {
                    this.name = "Ryan";
                  });
                  afterEach(function (done) {
                    this.name = null;
                    done(); // Can be async too!
                  });
                  it("should be Ryan", function () {
                    expect(this.name).to.equal("Ryan");
                  });
                });
                

Chai Assertions

  • Natural language syntax.
  • Chained assertions.

Chai API

The "BDD" API:

  • Chains: to, be, been, have
  • Groups: and
  • Assertions: a, equal, length, match
  • Modifiers: contain, not

Testing For Type Demo


                it("test for type name", function () {
                  expect("Hi Mom!")
                    .to.be.a("string");
                });
                
                it("test for instance constructor", function () {
                  expect({})
                    .to.be.an.instanceof(Object);
                });
                

**Make me Work** Demo


                it("test for instances", function () {
                  var Constructor = function () {};
                  expect("TODO: replace this string")
                    .to.be.an.instanceof(Constructor);
                });
                

Solution

it("test for instances", function () {
  var Constructor = function () {};
  expect(new Constructor())
    .to.be.an.instanceof(Constructor);
});

Testing Elements Demo


                it("tests for elements", function () {
                
                  expect(["dog", "cat"]).to.contain("dog")
                    .and.to.have.length(2)
                    .and.to.not.contain("horse");
                    
                });
                

Testing for Properties Demo


                it("tests for properties", function () {
                
                  expect({foo: {bar: 12}, fizz: {buzz: "3"}})
                    .to.have.property("foo");
                    
                });
                

**Make me Work** Demo


                it("tests for keys", function () {
                  expect({foo: {bar: 12}, fizz: { buzz: "3"}})
                    .to.have.keys(["foo", "bar"]);
                });
                

Solution

it("tests for keys", function () {
  expect({foo: {bar: 12}, fizz: { buzz: "3"}})
    .to.have.keys(["foo", "fizz"]);
});

Testing for Equality Demo


                it("tests for equality", function () {
                  expect(2+2).to.equal(4)
                    .and.to.not.equal(5);
                });
                

A function to test

We're going to camel case strings:

"fun-test-time" ➞ "funTestTime"

Camel Case Demo


                window.camel = function (val) {
                  // Uppercase the first character after a dash.
                  return val.replace(/-(.)/g, function (m, first) {
                    return first.toUpperCase();
                  });
                };
                
                console.log(camel("hello-there-friend"));
                console.log(camel("a-b-c-d"));
                

CamelCase Asserts Demo


                describe("camel", function () {
                  it("handles base cases", function () {
                    expect(camel("")).to.equal("");
                    expect(camel("a")).to.equal("a");
                  });
                  it("camel cases strings", function () {
                    expect(camel("a-b")).to.equal("aB");
                    expect(camel("hi-there")).to.match(/hiT/);
                    // [TRY] FAIL expect(camel("hi-there")).to.equal("nope");
                  });
                });
                

Sinon.JS Fakes

Dependencies, complexities? Fake it!

Sinon.JS API

Sinon.JS Spies Demo


                describe("camel", function () {
                  it("shows how spies work", function () {
                    var spy = sinon.spy(String.prototype, "toUpperCase");
                    
                    expect(spy.callCount).to.equal(0);
                    expect(camel("a-b")).to.equal("aB");
                    expect(spy.callCount).to.equal(1);
                    expect(spy.firstCall.returnValue).to.equal("B");
                    
                    spy.restore();
                  });
                });
                

Sinon.JS Stubs Demo


                describe("camel", function () {
                  it("stubs upper case", function () {
                    var stub = sinon.stub(String.prototype, "toUpperCase",
                      function () { return "FOO"; });
                      
                    expect(camel("a-bear")).to.equal("aFOOear");
                    expect(stub.callCount).to.equal(1);
                    
                    stub.restore();
                  });
                });
                

Sinon.JS Fake Servers Demo


                describe("fake servers", function () {
                  it("fakes XHR/server", function (done) {
                    var server = sinon.fakeServer.create();
                    
                    $.get("http://example.com/NO", function (data) {
                      expect(data).to.eql({ fake: "data!" });
                      done();
                    }).fail(function () {
                      done(new Error("Request failed"));
                    });
                    
                    server.respondWith("GET", "http://example.com/NO", [
                      200, {"Content-Type": "application/json"},
                      JSON.stringify({ fake: "data!" })
                    ]);
                    server.respond(); // [TRY] Remove - TIMEOUT.
                    server.restore(); // [TRY] Remove `server` - NETWORK.
                  });
                });
                

Sinon.JS Fake Timers Demo


                describe("fake timers", function () {
                  it("fakes time", function (done) {
                    var clock = sinon.useFakeTimers();
                    
                    setTimeout(done, 1500);
                    
                    clock.tick(1501); // [TRY] Remove - TIMEOUT.
                    clock.restore();
                    // [TRY] Remove `clock` entirely - SLOW.
                  });
                });
                

Running the Suite

Drive our frontend tests with real browsers and PhantomJS using Karma

Karma

# Mac/Linux
$ node_modules/.bin/grunt karma:mocha-fast
$ node_modules/.bin/grunt karma:mocha-all

# Windows
$ node_modules\.bin\grunt karma:mocha-fast
$ node_modules\.bin\grunt karma:mocha-windows

Continuous Integration

Hook tests to commits with: Travis CI

Build Status

More Test Topics

  • Fixtures: DOM, Data
  • Continuous Deployment
  • Coverage

Thanks!

Let us know what you think!


http://bit.ly/seattlejs-test-survey

@FormidableLabs | formidablelabs.com