The Evolution of Behavioral Driven Development

Created by Todd Wolfson / @twolfsn / github/twolfson

BDD Example

BDD Example

function Calculator() {
  this.sum = 0;
}

Calculator.prototype = {
  add: function (num) {
    this.sum += num;
    return this;
  },
  total: function () {
    return this.sum;
  }
};

module.exports = Calculator;

BDD Example

var assert = require('assert'),
    Calculator = require('./calculator.js');

describe('A calculator', function () {
  before(function () {
    this.calc = new Calculator();
  });

  it('has a total of 0', function () {
    var total = this.calc.total();
    assert.strictEqual(total, 0);
  });

  describe('adding 2 and 3', function () {
    before(function () {
      this.calc.add(2).add(3);
    });

    it('has a total of 5', function () {
      var total = this.calc.total();
      assert.strictEqual(total, 5);
    });
  });
});
(Sorry about the scrolling)

BDD Example

Successful tests with nyan reporter

Why are tests important?

Why do cars have brakes?

Why are tests important?

Why do cars have brakes?
So you can drive faster.

- Kevlin Henney

Absolving confusion

Time to unlearn misconceptions

Sudden clarity clarence

What are TDD and BDD?

  • Automated verification
  • Differences lie in format (e.g. JSON vs XML)

Test-first vs test-later

  • Unrelated to TDD and BDD
  • In some situations, test-first is appropriate
  • In others, test-later is appropriate

Code coverage

  • Code coverage = % code is touched during testing
  • In some cases, 100% is good to have
  • In other situations, lower coverage is appropriate

Implementation vs Methodology

  • PhantomJS is a testing harness for manipulating a browser.
  • CasperJS is a combo of a framework (TDD) and harness.
  • Selenium is a test runner, harness, and framework.

Nyan cat

Before automation

Manual testing

IBM logo

  • Cross-checking results
  • Quality assurance engineers

Birth of TDD

SUnit (1994)

Kent Beck

  • Kent Beck working in Smalltalk
  • Created various experiments
  • While consulting, converted latest concept into classical concept

SUnit (1994)


  Class: SetTestCase
      superclass: TestCase
      instance variables: empty full

  SetTestCase>>setUp
      empty := Set new.
      full := Set
          with: #abc
          with: 5

  SetTestCase>>testAdd
      empty add: 5.
      self should: [empty includes: 5]

  SetTestCase>>testRemove
      full remove: 5.
      self should: [full includes: #abc].
      self shouldnt: [full includes: 5]

  | suite |
  suite := TestSuite named: 'Set Tests'.
  suite addTestCase: (SetTestCase selector: #testAdd).
  suite addTestCase: (SetTestCase selector: #testRemove).
  ^suite
            
(Sorry about the scrolling)

JUnit (1995 - 2000)

JUnit logo

  • 1995, Kent Beck and Erich Gamma
  • 2000, junit.org is registered and Sourceforge'd

JUnit (1995 - 2000)


  public class MoneyTest extends TestCase {
      private Money f12CHF;
      private Money f14CHF;
      private Money f28USD;

      protected void setUp() {
          f12CHF= new Money(12, "CHF");
          f14CHF= new Money(14, "CHF");
          f28USD= new Money(28, "USD");
      }
  }

  public void testSimpleAdd() {
      Money m12CHF= new Money(12, "CHF");
      Money m14CHF= new Money(14, "CHF");
      Money expected= new Money(26, "CHF");
      Money result= m12CHF.add(m14CHF);
      assert(expected.equals(result));
  }

  TestSuite suite= new TestSuite();
  suite.addTest(new MoneyTest("testSimpleAdd"));
  TestResult result= suite.run();
            

Evolution into BDD

Dan North (2006)

Dan North

Wants to convert agile story template into scenarios

Dan North (2006)

Push it somewhere else

Dan North (2006)

As a [X]

I want [Y]

so that [Z]

Given some initial context,

When an event occurs,

Then ensure some outcomes.

jBehave (2006)

jBehave logo

  • Published by Dan North and Chris Matts

jBehave (2006)


  public class TraderSteps {

      private Stock stock;

      @Given("a stock of symbol $symbol and a threshold of $threshold")
      public void aStock(String symbol, double threshold) {
          stock = new Stock(symbol, threshold);
      }

      @When("the stock is traded at $price")
      public void theStockIsTradedAt(double price) {
          stock.tradeAt(price);
      }

      @Then("the alert status should be $status")
      public void theAlertStatusShouldBe(String status) {
          ensureThat(stock.getStatus().name(), equalTo(status));
      }

  }
            

RSpec (2006)

RSpec book image

  • Published by Steven Baker

RSpec (2006)


  describe Hash do
    before(:each) do
      @hash = Hash.new(:hello => 'world')
    end

    it "should return a blank instance" do
      Hash.new.should eql({})
    end

    it "should hash the correct information in a key" do
      @hash[:hello].should eql('world')
    end
  end
            

Cucumber (2008)

Cucumber logo

  • Gem by Aslak Hellesøy
  • First to separate specification from implementation

Cucumber (2008)


Feature: Addition
  In order to avoid silly mistakes
  As a math idiot
  I want to be told the sum of two numbers

  Scenario Outline: Add two numbers
    Given I have entered <input_1> into the calculator
    And I have entered <input_2> into the calculator
    When I press <button>
    Then the result should be <output> on the screen
            

Before do
  @calc = Calculator.new
end

Given /I have entered (\d+) into the calculator/ do |n|
  @calc.push n.to_i
end

When /I press (\w+)/ do |op|
  @result = @calc.send op
end

Then /the result should be (.*) on the screen/ do |result|
  @result.should == result.to_f
end
            

Flat files (???)

  • Tests represented as input/output files

  • Language and framework agnostic

  • In the words of substack:

    It's the universal interface. Standard in and standard out.

Flat files (???)


  // Test data
  {
    name: "Chris",
    value: 10000,
    taxed_value: 6000,
    in_ca: true
  }
            

  // Mustache template
  Hello {{name}}
  You have just won ${{value}}!
  {{#in_ca}}
  Well, ${{ taxed_value }}, after taxes.
  {{/in_ca}}
            

  // Expected output
  Hello Chris
  You have just won $10000!
  Well, $6000, after taxes.
            

Modern time. Modern space.

var now = new Date();

JUnit (circa 2012)


  public class MyClassTest {

    @BeforeClass
    public static void testSetup() {
    }

    @AfterClass
    public static void testCleanup() {
      // Teardown for data used by the unit tests
    }

    @Test(expected = IllegalArgumentException.class)
    public void testExceptionIsThrown() {
      MyClass tester = new MyClass();
      tester.multiply(1000, 5);
    }

    @Test
    public void testMultiply() {
      MyClass tester = new MyClass();
      assertEquals("10 x 5 must be 50", 50, tester.multiply(10, 5));
    }
  }
            

qunit


  test( "ok test", function() {
    ok( true, "true succeeds" );
    ok( "non-empty", "non-empty string succeeds" );
    ok( false, "false fails" );
    ok( 0, "0 fails" );
    ok( NaN, "NaN fails" );
    ok( "", "empty string fails" );
    ok( null, "null fails" );
    ok( undefined, "undefined fails" );
  });
            

nodeunit


  module.exports = {
      setUp: function (callback) {
          this.foo = 'bar';
          callback();
      },
      tearDown: function (callback) {
          // clean up
          callback();
      },
      test1: function (test) {
          test.equals(this.foo, 'bar');
          test.done();
      }
  };
            

Expresso


  module.exports = {
      'test String#length': function(beforeExit, assert) {
        assert.equal(6, 'foobar'.length);
      }
  };
            

tape


  test('timing test', function (t) {
      t.plan(2);

      t.equal(typeof Date.now, 'function');
      var start = Date.now();

      setTimeout(function () {
          t.equal(Date.now() - start, 100);
      }, 100);
  });
            

Jasmine


  describe("A suite is just a function", function() {
    var a;

    it("and so is a spec", function() {
      a = true;

      expect(a).toBe(true);
    });
  });
            

  describe("Asynchronous specs", function() {
    var value, flag;

    it("should support async execution [...]", function() {

      runs(function() {
        flag = false;
        value = 0;

        setTimeout(function() {
          flag = true;
        }, 500);
      });

      waitsFor(function() {
        value++;
        return flag;
      }, "The Value should be incremented", 750);

      runs(function() {
        expect(value).toBeGreaterThan(0);
      });
    });
  });
            

Cucumber.js


  Feature: Example feature
    As a user of cucumber.js
    I want to have documentation on cucumber
    So that I can concentrate on building awesome applications

    Scenario: Reading documentation
      Given I am on the Cucumber.js Github repository
      When I go to the README file
      Then I should see "Usage" as the page title
            

  module.exports = function () {
    this.World = require("../support/world.js").World; // overwrite default World constructor

    this.Given(/^I am on the Cucumber.js Github repository$/, function(callback) {
      // Express the regexp above with the code you wish you had.
      // `this` is set to a new this.World instance.
      // i.e. you may use this.browser to execute the step:

      this.visit('http://github.com/cucumber/cucumber-js', callback);

      // The callback is passed to visit() so that when the job's finished, the next step can
      // be executed by Cucumber.
    });

    this.When(/^I go to the README file$/, function(callback) {
      // Express the regexp above with the code you wish you had. Call callback() at the end
      // of the step, or callback.pending() if the step is not yet implemented:

      callback.pending();
    });

    this.Then(/^I should see "(.*)" as the page title$/, function(title, callback) {
      // matching groups are passed as parameters to the step definition

      if (!this.isOnPageWithTitle(title))
        // You can make steps fail by calling the `fail()` function on the callback:
        callback.fail(new Error("Expected to be on page with title " + title));
      else
        callback();
    });
  };
            

vows


  vows.describe('The Good Things').addBatch({
      'A banana': {
          topic: new(Banana),

          'when peeled *synchronously*': {
              topic: function (banana) {
                  return banana.peelSync();
              },
              'returns a `PeeledBanana`': function (result) {
                  assert.instanceOf (result, PeeledBanana);
              }
          },
          'when peeled *asynchronously*': {
              topic: function (banana) {
                  banana.peel(this.callback);
              },
              'results in a `PeeledBanana`': function (err, result) {
                  assert.instanceOf (result, PeeledBanana);
              }
          }
      }
  }).export(module); // Export the Suite
            

mocha


    describe('Array', function(){
      before(function(){
        // ...
      });

      describe('#indexOf()', function(){
        it('should return -1 when not present', function(){
          [1,2,3].indexOf(4).should.equal(-1);
        });
      });
    });
            

    describe('User', function(){
      describe('#save()', function(){
        it('should save without error', function(done){
          var user = new User('Luna');
          user.save(function(err){
            if (err) throw err;
            done();
          });
        });
      });
    });
            

intern


define([
  'intern!bdd',
  'intern/chai!expect',
  '../Request'
], function (bdd, expect, Request) {
  with (bdd) {
    describe('demo', function () {
      var request,
        url = 'https://github.com/theintern/intern';

      // before the suite starts
      before(function () {
        request = new Request();
      });

      // before each test executes
      beforeEach(function () {
        request.reset();
      });

      // after the suite is done
      after(function () {
        request.cleanup();
      });

      // multiple methods can be registered and will be executed in order of registration
      after(function () {
        if (!request.cleaned) {
          throw new Error('Request should have been cleaned up after suite execution.');
        }

        // these methods can be made asynchronous as well by returning a promise
      });

      // asynchronous test for Promises/A-based interfaces
      it('should demonstrate a Promises/A-based asynchronous test', function () {
        // `getUrl` returns a promise
        return request.getUrl(url).then(function (result) {
          expect(result.url).to.equal(url);
          expect(result.data.indexOf('next-generation') > -1).to.be.true;
        });
      });

      // asynchronous test for callback-based interfaces
      it('should demonstrate a callback-based asynchronous test', function () {
        // test will time out after 1 second
        var dfd = this.async(1000);

        // dfd.callback resolves the promise as long as no errors are thrown from within the callback function
        request.getUrlCallback(url, dfd.callback(function () {
          expect(result.url).to.equal(url);
          expect(result.data.indexOf('next-generation') > -1).to.be.true;
        });

        // no need to return the promise; calling `async` makes the test async
      });

      // nested suites work too
      describe('xhr', function () {
        // synchronous test
        it('should run a synchronous test', function () {
          expect(request.xhr).to.exist;
        });
      });
    });
  }
});
            

Time to experiment

doubleshot (2013)

Wants:

  • Slimmer tests
  • Vows-like syntax
  • Cross-environment

Road:

  • Wrote cross-compilers from vows to mocha

    • crossbones (runtime)
    • sculptor (preprocessor)
  • Discovered cool features

doubleshot (2013)

function Calculator() {
  this.sum = 0;
}

Calculator.prototype = {
  add: function (num) {
    this.sum += num;
    return this;
  },
  total: function () {
    return this.sum;
  }
};

module.exports = Calculator;

doubleshot (2013)

module.exports = {
  'A calculator': {
    'begins with a total of 0': true,
    'adding 2 and 3': {
      'returns 5 as the total': true
    }
  }
};
module.exports = {
  'A calculator': function () {
    this.calc = new Calculator();
  },
  'begins with a total of 0': function () {
    assert.strictEqual(this.calc.total(), 0);
  },
  'adding 2 and 3': ['add 2', 'add 3'],
  'add 2': function () {
    this.calc.add(2);
  },
  'add 3': function () {
    this.calc.add(3);
  },
  'returns 5 as the total': function () {
    assert.strictEqual(this.calc.total(), 5);
  }
};

BDD Example

Successful doubleshot tests with nyan reporter

The Evolution of Behavioral Driven Development

Created by Todd Wolfson / @twolfsn / github/twolfson