testing-zen



testing-zen

0 0


testing-zen


On Github timruffles / testing-zen

Testing Zen

@timruffles @sidekicksrc

@timruffles founder of SidekickJS

Testing == Koans

advanced side of testing

audience like you knows assert()

analogy: koans

Koans can be frustrating

Feels like you're stupid

Afterwards, you realise you were

Both can be violent

Both can be mistaken for ends

A koan for you:

Four friends planned 7 days of silence, on the forth day one shouted "fix the lamp"! One of his friends said, "we are not to talk!", the other "you two are stupid". The final friend smugly remarked, "I am the only one who remained silent".

Test 0

The master asked "this code has a failing test". The student "but master, this code has no tests!". Walking away, the master said "Fix that failing test, or I'll not merge!"

Can you test it?

Very useful test

Sometimes, easily

function sprintf(str) {
  var params = [].slice.call(arguments,1)
  var types = {
    "%s": function(x) { return x + "" }
  }
  var matches = 0
  return str.replace(/(%[is])/g,function(match) {
    return types[match](params[matches++])
  })
}

it("formats correctly",function() {
  assert.equal("MU!",sprintf("%s","MU!"));
})

Often this simple?

Get real

'generateResetToken': function (req, res) {
    var email = req.body.email;

    api.users.generateResetToken(email).then(function (token) {
        var siteLink = '<a href="' + config().url + '">' + config().url + '</a>',
            resetUrl = config().url.replace(/\/$/, '') +  '/ghost/reset/' + token + '/',
            resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
            message = {
                to: email,
                subject: 'Reset Password',
                html: '<p><strong>Hello!</strong></p>' +
                      '<p>A request has been made to reset the password on the site ' + siteLink + '.</p>' +
                      '<p>Please follow the link below to reset your password:<br><br>' + resetLink + '</p>' +
                      '<p>Ghost</p>'
            };

        return mailer.send(message);
    }).then(function success() {
        var notification = {
            type: 'success',
            message: 'Check your email for further instructions',
            status: 'passive',
            id: 'successresetpw'
        };

        return api.notifications.add(notification).then(function () {
            res.json(200, {redirect: config.paths().webroot + '/ghost/signin/'});
        });

    }, function failure(error) {
        // TODO: This is kind of sketchy, depends on magic string error.message from Bookshelf.
        // TODO: It's debatable whether we want to just tell the user we sent the email in this case or not, we are giving away sensitive info here.
        if (error && error.message === 'EmptyResponse') {
            error.message = "Invalid email address";
        }

        res.json(401, {error: error.message});
    });
},
'generateResetToken': function (req, res) {
    var email = req.body.email;

    api.users.generateResetToken(email).then(function (token) {
        var siteLink = '<a href="' + config().url + '">' + config().url + '</a>',
            resetUrl = config().url.replace(/\/$/, '') +  '/ghost/reset/' + token + '/',
            resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
            message = {
                to: email,
                subject: 'Reset Password',
                html: '<p><strong>Hello!</strong></p>' +
                      '<p>A request has been made to reset the password on the site ' + siteLink + '.</p>' +
                      '<p>Please follow the link below to reset your password:<br><br>' + resetLink + '</p>' +
                      '<p>Ghost</p>'
            };

        return mailer.send(message);
    }).then(function success() {
      // ...
    }, function failure(error) {
      // ...
    });
},
'generateResetToken': function (req, res) {
    var email = req.body.email;

    api.users.generateResetToken(email).then(function (token) {
      // ...
    }).then(function success() {
      // ...
    }, function failure(error) {
        // TODO: This is kind of sketchy, depends on magic string error.message from Bookshelf.
        // TODO: It's debatable whether we want to just tell the user we sent the email in this case or not, we are giving away sensitive info here.
        if (error && error.message === 'EmptyResponse') {
            error.message = "Invalid email address";
        }

        res.json(401, {error: error.message});
    });
},
'generateResetToken': function (req, res) {
    var email = req.body.email;

    api.users.generateResetToken(email).then(function (token) {
      // ...
    }).then(function success() {
        var notification = {
            type: 'success',
            message: 'Check your email for further instructions',
            status: 'passive',
            id: 'successresetpw'
        };

        return api.notifications.add(notification).then(function () {
            res.json(200, {redirect: config.paths().webroot + '/ghost/signin/'});
        });
    }, function failure(error) {
      // ...
    });
},

If not, why?

JS is easy to test

If you know the sound of one hand clapping

If you KISS

'generateResetToken': function (req, res) {
    var email = req.body.email;
    api.users.generateResetToken(email).then(
      api._sendMessage.bind(null,config,email)
    ).then(
      api._notifySuccess(res,{
        type: 'success',
        message: 'Check your email for further instructions',
        status: 'passive',
        id: 'successresetpw'
      })
      , api._handleFailure
    );
},

Unit tests need units

Code can hurt tests

Units have one job

Isolate

until it's 'too easy'

a) Visiblity

Name and reveal

Async makes processes clear

b) Partials

Partial

  • JS uses higher-order-functions
  • Functions that take or return functions
  • x.bind() creates a 'partially applied' version of x
  • First arg is JS's OO's fault
var helloName = sprintf.bind(null,"Hello %s")
helloName("Sue") // "Hello Sue"

Why?

Steps with dependencies

'generateResetToken': function (req, res) {
    var email = req.body.email;
    api.users.generateResetToken(email).then(function (token) {
      // ...
      message = {
          to: email,
      // ...
    }).then(function success() {

    // ...
'generateResetToken': function (req, res) {
    var email = req.body.email;
    api.users.generateResetToken(email).then(
      api._sendMessage.bind(null,config,email)
    ).then(function success() {
_sendMessage: function(config,email,token) {
  // ...
}

Tests have taught us modularity

Just functional test!

No x 3

Functional tests are limited

Edge-cases

Functional tests are slow

Functional tests don't hurt

?!

Tests are design critique

Untestable: non-modular by definition

Reveals coupling

var defaultConfig =  require('../../../config');

describe("Mail", function () {

    beforeEach(function () {
        // Mock config and settings
        fakeConfig = _.extend({}, defaultConfig);

// ...

it('should setup SMTP transport on initialization', function (done) {
  fakeConfig[process.env.NODE_ENV].mail = SMTP;
  mailer.init().then(function () {
      mailer.should.have.property('transport');
      mailer.transport.transportType.should.eql('SMTP');
      mailer.transport.sendMail.should.be.a.function;
      done();
  }).then(null, done);
});

Temporal coupling

Please don't use modules as mutable globals

Brutality

Desiring to show his wisdom, the student said: "The mind, Buddha and world are merely illusions. There is no giving and nothing to be received." The master sat and smoked, and then struck the youth with his pipe. The youth was angry. The master asked: "If the world is merely an illusion, where does this anger come from?"

Creative, destructive

Destructive mind

  • All code is broken, I just haven't found out how yet
  • I punish my code because I take the blame for it
  • The errors will remain, so I must squeeze the places to hide

Learn C the Hard Way, Zed Shaw

False positives

Coincidence

it("creates a user",function(done) {
  app.post("/users",validUserAttrs)
     .then(function() {
       assert.equal(1,User.count())
       done()
     },done)
})

Still

it("creates a user",function(done) {
  assert.change(function(check) {
    app.post("/users",validUserAttrs)
       .then(function() {
         check()
         done()
       },done)
  },function() { return User.count() })
})

Again

it("creates a user",function(done) {
  assert.change(function(check) {
    app.post("/users",validUserAttrs)
       .then(function() {
         check()
         assert.equal(validUserAttrs.name,User.first().name)
         done()
       },done)
  },function() { return User.count() })
})

Confident?

it("creates a user",function(done) {
  assert.change(function(check) {
    app.post("/users",validUserAttrs)
       .then(function() {
         check()
         assert.equal(validUserAttrs.name,User.first().name)
         done()
       },done)
   },function() { return User.count() },{by: 1})
})

Edge-cases

  • Numbers: 0, Infinity, negative, irrational/rational
  • Collections: empty, one, many
  • Strings: empty... and a koan

Endless Koan of JS

I have created a string as long as time itself, yet it is not there
var string = "   \t\t\t\t \n \n \t \t\t\t"
while(string == false)
  string += ["\t","\n"," "][Math.random() * 3 | 0]

Table-driven tests

var possibleGitObjectNames = [
  { name: "aeff938482", expected: git.SHA },
  { name: "master", expected: git.REF },
]

possibleGitObjectNames.forEach(function(setup) {
  it(util.format("correctly identifies '%s' as a '%s'",
    setup.name,setup.expected),function() {
    assert.equal(setup.expect,
      git.identifyObjectNameType(setup.name))
  })
})

var invalidObjectNames = [
  "../",
  "refs/heads/master",
  "\t"
]

invalidObjectNames.forEach(function(name) {
  it(util.format("identifies '%s' as an invalid object name",name),function() {
    assert.throws(function() {
      git.identifyObjectNameType(name)
    })
  })
})

Functional tests

Frontend

Bang for buck

Real browsers

Command line

Karma

Getting the kit

npm install --save phantomjs karma karma-mocha  karma-chai
vim karma.conf.js
module.exports = function(config) {
  config.set({
    frameworks: ['mocha','chai'],
    browsers: ['Chrome', 'Firefox'],
    files: [
      'vendor/*.js',
      'src/*.js',
      'tests/*_test.js'
    ],
    client: {
      mocha: {
        ui: 'bdd'
      }
    }
  });
};

Hello Karma

describe("karma testing",function() {

  it("is clearly in the browser, I'm parsing URLs" +
    "with an anchor tag",function() {
    var urlStr = "http://example.com"
    var url = parseUrl(urlStr) 
    assert.equal(url.hostname,"example.com")
  })

})

function parseUrl(str) {
  if(!parseUrl.parser)
    parseUrl.parser = document.createElement("a")
  parseUrl.parser.href = str
  return ["hostname","protocol","path"].reduce(function(h,k) {
    h[k] = parseUrl.parser[k]
    return h
  },{})
}

Hello widget

describe("annoying cookie widget",function() {
  it("can be commanded to leave by the user",function() {
    var widget = new AnnoyingCookie
    var cares = true
    widget.ondoesnotcare = function() {
      cares = false
    }
    widget.render()
    $(widget.el).find(".does_not_care").click()
    assert.isFalse(cares,"user unable to dismiss " +
      " stupid cookie warning")
  })
})

Hello node

  • node-test/app_test.js
var app = require("../app.js")
var request = require("supertest")

describe("math server 1.1",function() {
  describe("addition",function() {
    it("can add",function(done) {
      request(app)
        .get('/add/10/15')
        .expect(/answer/)
        .expect(/:\s*25/)
        .expect(200,done)
    })
    // I wonder if we should test other types of numbers?
    // are there edge cases for numbers at all?
    it("validates numbers",function(done) {
      request(app)
        .get('/add/spoon/15')
        .expect(/error/)
        .expect(400,done)
    })
  })
})

What not to test

Library code

jQuery, ORM etc

So stub?

No: wrap

Are tests enough?

Test the tests?

Coverage

How?

Annotates source code

Be honest

Make uncovered code break the build

Diminishing returns

CI server

Tests unrun < no tests

Zen mind is not zen mind

Beginner mind

TDD

Great for known problems

BDD

Focus on behaviour

Focus on... syntax?

RSpec's influence

Vanity of small differences

test("#hostname",function() {
  assert("example.com" === url.hostname)
})
test("#hostname",function() {
  assert.equal("example.com",url.hostname)
})
test("provides access to hostname",function() {
  assert.equal("example.com",url.hostname)
})
it("provides access to hostname",function() {
  expect(url.hostname).to.equal("example.com")
})
it("provides access to hostname",function() {
  url.hostname.should.equal("example.com")
})

In 2005 I drunkenly released a dumb hack. It was called RSpec. You are victims on one of the biggest trolls ever committed. You’re welcome.

— Steven R. Baker (@srbaker) June 13, 2013

@shinypb I do BDD in Ruby with MiniTest. And I don’t use the RSpec syntax.

— Steven R. Baker (@srbaker) June 15, 2013

Halting problem

  • multiply by all languages and frameworks you use
x.should.equal(y)
x.should.be.equalTo(y)
x.should.be.equal.to(y)
expect(x).to.equal(y)
expect(x).to.be.equalTo(y)
expect(x).equals(y)
Given the URL '://example.com/links/?uri_id=cow'
Then as a programmer
I can access the hostname as 'example.com'

Writing tests in Regex!

Cargo cult

Fear of change

Investment

Koans have broad ramification

Any answers for our koan?

Four friends planned 7 days of silence, on the forth day one shouted "fix the lamp"! One of his friends said, "we are not to talk!", the other "you two are stupid". The final friend smugly remarked, "I am the only one who remained silent".

Confusing means with ends

Don't get attached to means

Methodologies

Languages

Platforms

Paradigms

...professions

means != ends

Zen mind is not zen mind

@timruffles, @sidekicksrc