Code Fellows - Learn Frontend Testing

Learn Frontend Testing

Code Fellows - Dec. 17th, 2013

Ryan Roemer@ryan_roemer

@FormidableLabs

Sponsors

Formidable Labs
DevLocal

Motivation

Web applications are increasingly becoming frontend heavy.

We need to verify app logic and behavior, and that means braving the browser.

So let's test

Backend is straightforward and easy

... but what about the frontend?

Frontend testing

Frontend testing is difficult and error-prone.

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

But getting better

Backbone.js Testing

... so let's get started with a modern frontend test stack.

Get the code

github.com/FormidableLabs/codefellows-frontend-testing

              $ git clone https://github.com/FormidableLabs/
                          codefellows-frontend-testing.git
                          

Overview

  • Installation and test page
  • Suites
  • Assertions
  • Fakes
  • Automation

We will learn how to

  • Hook frontend JS to tests
  • Write assertions against behavior
  • Fake application behavior
  • Run and verify the tests

Things we're not going to cover

  • TDD / BDD
  • Application development
  • Functional testing
  • Performance testing

Coding time

We're going to say hello:

"Code Fellows" ➞ "Hello Code Fellows!"


And camel case strings:

"fun-test-time" ➞ "funTestTime"

Set up your project


              # Copy the skeleton application.
              $ cp -r skeleton MY_APP_NAME

Project Structure

Using with the "skeleton" application.


              MY_APP_NAME/
                js/
                  app/
                    hello.js
                    camel.js
                  lib/
                    chai.js
                    mocha.js
                    mocha.css
                    sinon.js
                index.html

Hello!

skeleton/js/app/hello.js
// Hello [VALUE]!
var hello = function (val) {
  return "Hello " + val + "!";
};

Camel Case

skeleton/js/app/camel.js
// Camel case a string.
var camel = function (val) {
  // Uppercase the first character after a dash.
  return val.replace(/-(.)/g, function (m, first) {
    return first.toUpperCase();
  });
};

Demo

skeleton/index.html

Test harness

Test Libraries

  • Mocha (v1.13.0): Framework
  • Chai (v1.7.3): Assertions
  • Sinon.JS (v1.8.1): Fakes - spies and stubs

Directory layout

A combined structure.


              MY_APP_NAME/
                js/
                  app/
                  lib/
                  spec/
                    hello.spec.js
                    *.spec.js
                test.html
                index.html

The test page

Create a test "driver" web page.

example/test.html

$ touch MY_APP_NAME/test.html

test.html

<html>
  <head>
    <title>Frontend Testing</title>
    <!-- Application libraries. -->
    <script src="js/app/hello.js"></script>
    <script src="js/app/camel.js"></script>
    <!-- Test styles and libraries. -->
    <link rel="stylesheet"
          href="js/lib/mocha.css" />
    <script src="js/lib/mocha.js"></script>
    <script src="js/lib/chai.js"></script>
    <script src="js/lib/sinon.js"></script>

test.html

    <!-- Test Setup -->
    <script>
      // Set up Chai and Mocha.
      window.expect = chai.expect;
      mocha.setup("bdd");
      
      // Run tests on window load.
      window.onload = function () {
        mocha.run();
      };
    </script>

test.html

    <!-- Tests. -->
    <!-- ... spec script includes go here ... -->
  </head>
  <body>
    <div id="mocha"></div>
  </body>
</html>

example/test-empty.html

Mocha Suites, Specs

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

Suites, Specs

test-mocha.html | mocha-suite.spec.js

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

describe("top-level", function () {
  describe("nested", function () {
    it("is slow and async", function (done) {
      setTimeout(function () { done(); }, 300);
    });
  });
});

Setup, Teardown

test-mocha.html | mocha-setup.spec.js

describe("setup/teardown", function () {
  before(function (done) { done(); });
  beforeEach(function () {});

  after(function (done) { done(); });
  afterEach(function () {});

  it("should test something");
});

Chai Assertions

  • Natural language syntax.
  • Chained assertions.

Chai API

The "bdd" API:

  • Chains: to, be, been, have
  • Groups: and
  • Basics: a, equal, length, match

Hello!

test-hello.html | hello.spec.js

describe("hello", function () {
  it("should say hello", function () {
    expect(hello("World"))
      .to.be.a("string").and
      .to.equal("Hello World!").and
      .to.have.length(12).and
      .to.match(/He[l]{2}/);
  });
});

Camel Case

test-camel.html | camel.spec.js

describe("camel", function () {
  it("handles base cases", function () {
    expect(camel("")).to.equal("");
    expect(camel("single")).to.equal("single");
  });
  it("handles dashed cases", function () {
    expect(camel("a-b-c")).to.equal("aBC");
    expect(camel("one-two")).to.equal("oneTwo");
  });
});

More Chai

test-chai.html | chai.spec.js | chai-fail.spec.js

describe("chai", function () {
  it("asserts", function () {
    expect(["one", "two"]).to.contain("two");
    expect({foo: {bar: 12}})
      .to.have.deep.property("foo.bar", 12);
  });
});
describe("chai", function () {
  it("fails", function () {
    expect("one").to.equal("two");
  });
});

Sinon.JS Fakes

Dependencies, complexities? Fake it!

Sinon.JS Spy

test-sinon.html | camel-spy.spec.js | camel.js

describe("camel", function () {
  it("spies upper case", 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 Stub

test-sinon.html | camel-stub.spec.js | camel.js

describe("camel", function () {
  it("stubs upper case", function () {
    var stub = sinon.stub(String.prototype, "toUpperCase",
      function () { return "FOO"; });

    expect(camel("a-b")).to.equal("aFOO");
    expect(stub.callCount).to.equal(1);

    stub.restore();
  });
});

Automation

Drive our frontend tests with PhantomJS using Mocha-PhantomJS

Prep test.html

Update the test.html file:


              window.onload = function () {
                (window.mochaPhantomJS || mocha).run();
              };

Headless!

Install and drive tests from the command line:


              $ npm install mocha-phantomjs
              $ node_modules/.bin/mocha-phantomjs \
                MY_APP_NAME/test.html

... and that's all for now!

What we've covered

  • Test harness
  • Suites, specs
  • Assertions
  • Fakes
  • Automation

Additional Topics

  • Advanced testing: DOM, fixtures
  • TDD / BDD
  • Functional testing
  • Performance testing
  • Continuous Integration: (Travis CI)

Thanks!

Ryan Roemer@ryan_roemer