Unexpected

The extensible assertion library

Started in 2013 as an experiment.

Used by One.com from v1.0.4
and is used for all JavaScript testing now.

Unexpected is starting to be used by other companies.

A small but very active and helpful community.

Find Gitter and Github links on http://unexpected.js.org.

We have great documentation!

http://unexpected.js.org

Many plugins that cover most functionality.

Syntax matters

programs_should_be_written_for_people_to_read_and_only_incidentally_for_machines_to_execute
programs.should.be.written.for.people.to.read.and.only.incidentally.for.machines.to.execute
programsShouldBeWrittenForPeopleToReadAndOnlyIncidentallyForMachinesToExecute
Programs should be written for people to read, and only incidentally for machines to execute.

— Abelson and Sussman

Assertions are just text - feel free to express yourself.

expect(obj, 'to have keys', 'foo', 'bar');

Especially important when you are doing complicated stuff:

return expect(
  fs.createReadStream('suboptimal.png'),
  'when piped through',
  new OptiPng(['-o7']),
  'to yield output satisfying', function (optiPngBuffer) {
    expect(optiPngBuffer.length, 'to be within', 0, 152);
  }
);

A nice side-effect of using strings:

expect(42, 'to not be', 24);
Unknown assertion 'to not be', did you mean: 'not to be'

Context is important

function Person() {
  this.names = Array.prototype.slice.call(arguments);
  Object.defineProperty(this, 'fullName', {
    get: function () {
      return this.names.join(' ');
    }
  });
}
 
var sune = new Person('Sune', 'Sloth', 'Simonsen');

expect(sune, 'to satisfy', { fullName: 'Simonsen, Sune Sloth' });
expected Person({ names: [ 'Sune''Sloth''Simonsen' ] })
to satisfy { fullName'Simonsen, Sune Sloth' }
 
Person({
  
names: [ 'Sune''Sloth''Simonsen' ],
  
fullName:
  
'Sune Sloth Simonsen' 
//
//
//
//
 
should equal 
'Simonsen, Sune Sloth'
 
Sune Sloth Simonsen
Simonsen, Sune Sloth
})

Being precise makes a difference

expect([ 0, 1, 2, 4, 5], 'to equal', [ 1, 2, 3, 4]);
expected [ 01245 ] to equal [ 1234 ]
 
[
  
0// should be removed
1,
2,
//
 
missing 
3
4,
5 // should be removed
]

We even diff buffers:

expect(new Buffer('wat?', 'utf-8'),
       'to equal',
       new Buffer('what?', 'utf-8'));
expected Buffer([0x77, 0x61, 0x74, 0x3F])
to equal Buffer([0x77, 0x68, 0x61, 0x74, 0x3F])
 
77 61 74 3F                                      │wat?
77 68 61 74 3F                                   │what?

Generally we try to be as helpful as possible:

expect('Hello ugly world!', 'not to match', /ugly/);
expected 'Hello ugly world!' not to match /ugly/
 
Hello ugly world!

Invalidates best practices

There should only be one assertion per test!

A single assert per unit test is a great way to test the reader's ability to scroll up and down.

A test should be concise and readable.

Match your object hierarchy against a specification:

var sune = { name: 'Sune', gender: 'mail', age: 35, kids: 2 };
 
expect(sune, 'to satisfy', {
  name: /.+/,
  age: expect.it('to be positive'),
  gender: /male|female/
});

expected { name'Sune'gender'mail'age35kids2 } to satisfy
{
  
name/.+/,
age
expect.it('to be positive'),
gender/male|female/
}
 
{
  
name'Sune',
  
gender'mail'
//
 
should match 
/male|female/
  
age35,
  
kids2
}

The world is not sequential

Asynchronous assertions:

it('saves a magicpen image with correct metadata', function () {
  return expect(
    'magic-pen-6-colours.jpg',
    'to have metadata satisfying', {
      format: 'JPEG',
      'Channel Depths': { Gray: '8 bits' },
      size: { width: 200, height: 400 }
    }
  });
});

http://unexpected.js.org/unexpected-image/

...and they compose:

it ('produces the correct png output', function () {
  return expect(
    createPngImageStream('bar.tiff'),
    'to yield output satisfying',
    expect.it('to resemble', 'bar.png', {
      mismatchPercentage: expect.it('to be less than', 10)
    }).and('to have metadata satisfying', {
      format: 'PNG'
    })
  );
});

http://unexpected.js.org/unexpected-stream/

Extensible from the ground up

Adding assertions

var arr = [1, 2, 3];
 
expect(arr, 'to equal', [].concat(arr).sort());
expect.addAssertion('<array> to be sorted <function?>',
                    function (expect, arr, cmp) {
  expect(arr, 'to equal', [].concat(arr).sort(cmp));
});
expect([1, 2, 3], 'to be sorted');
expect([3, 2, 1], 'to be sorted', function (a, b) {
  return b - a;
});
expect([2, 1, 3], 'to be sorted');
expected [ 213 ] to be sorted
 
[
┌─▷
└──
 
 
2,
 
1// should be moved
 
3
]

Adding types

var moment = require('moment');
expect(moment(31337), 'to equal', moment(1337));
expected
Moment({
  
_isAMomentObjecttrue_i31337_isUTCfalse,
_pf: {
  
emptyfalseunusedTokens: [], unusedInput: [],
overflow-2charsLeftOver0nullInputfalse,
invalidMonthnullinvalidFormatfalse,
userInvalidatedfalseisofalseparsedDateParts: [],
meridiemnull
},
_locale: Locale({
  
_calendar: {
  
sameDay'[Today at] LT'nextDay'[Tomorrow at] LT',
nextWeek'dddd [at] LT'lastDay'[Yesterday at] LT',
lastWeek'[Last] dddd [at] LT'sameElse'L'
},
_longDateFormat: {
  
LTS'h:mm:ss A'LT'h:mm A'L'MM/DD/YYYY',
LL'MMMM D, YYYY'LLL'MMMM D, YYYY h:mm A',
LLLL'dddd, MMMM D, YYYY h:mm A'
},
_invalidDate'Invalid date',
ordinalfunction ordinal(number) {
  var b = number % 10,
    output = (toInt(number % 100 / 10) === 1) ? 'th' :
    (=== 1) ? 'st' :
    (=== 2) ? 'nd' :
    (=== 3) ? 'rd' : 'th';
  return number + output;
},
_ordinalParse/\d{1,2}(th|st|nd|rd)/,
_relativeTime: {
  
future'in %s'past'%s ago's'a few seconds',
m'a minute'mm'%d minutes'h'an hour',
hh'%d hours'd'a day'dd'%d days'M'a month',
MM'%d months'y'a year'yy'%d years'
},
_months: [
  
'January',
  
'February',
  
'March',
  
'April',
  
'May',
  
'June',
  
'July',
  
'August',
  
'September',
  
'October',
  
'November',
  
'December'
],
_monthsShort: [
  
'Jan',
  
'Feb',
  
'Mar',
  
'Apr',
  
'May',
  
'Jun',
  
'Jul',
  
'Aug',
  
'Sep',
  
'Oct',
  
'Nov',
  
'Dec'
],
_week: { dow0doy6 },
_weekdays: [
  
'Sunday',
  
'Monday',
  
'Tuesday',
  
'Wednesday',
  
'Thursday',
  
'Friday',
  
'Saturday'
],
_weekdaysMin: [ 'Su''Mo''Tu''We''Th''Fr''Sa' ],
_weekdaysShort: [ 'Sun''Mon''Tue''Wed''Thu''Fri''Sat' ],
_meridiemParse/[ap]\.?m?\.?/i_abbr'en',
_config: {
  
calendar: {
  
sameDay'[Today at] LT'nextDay'[Tomorrow at] LT',
nextWeek'dddd [at] LT'lastDay'[Yesterday at] LT',
lastWeek'[Last] dddd [at] LT'sameElse'L'
},
longDateFormat: {
  
LTS'h:mm:ss A'LT'h:mm A'L'MM/DD/YYYY',
LL'MMMM D, YYYY'LLL'MMMM D, YYYY h:mm A',
LLLL'dddd, MMMM D, YYYY h:mm A'
},
invalidDate'Invalid date',
ordinalfunction ordinal(number) {
  var b = number % 10,
    output = (toInt(number % 100 / 10) === 1) ? 'th' :
    (=== 1) ? 'st' :
    (=== 2) ? 'nd' :
    (=== 3) ? 'rd' : 'th';
  return number + output;
},
ordinalParse/\d{1,2}(th|st|nd|rd)/,
relativeTime: {
  
future'in %s'past'%s ago's'a few seconds',
m'a minute'mm'%d minutes'h'an hour',
hh'%d hours'd'a day'dd'%d days'M'a month',
MM'%d months'y'a year'yy'%d years'
},
months: [...], monthsShort: [...],
week: { dow0doy6 },
weekdays: [
  
'Sunday',
  
'Monday',
  
'Tuesday',
  
'Wednesday',
  
'Thursday',
  
'Friday',
  
'Saturday'
],
weekdaysMin: [ 'Su''Mo''Tu''We''Th''Fr''Sa' ],
weekdaysShort: [ 'Sun''Mon''Tue''Wed''Thu''Fri''Sat' ],
meridiemParse/[ap]\.?m?\.?/iabbr'en'
},
_ordinalParseLenient/\d{1,2}(th|st|nd|rd)|\d{1,2}/
}),
_dnew Date('Thu, 01 Jan 1970 00:00:31.337 GMT')
})
to equal
Moment({
  
_isAMomentObjecttrue_i1337_isUTCfalse,
_pf: {
  
emptyfalseunusedTokens: [], unusedInput: [],
overflow-2charsLeftOver0nullInputfalse,
invalidMonthnullinvalidFormatfalse,
userInvalidatedfalseisofalseparsedDateParts: [],
meridiemnull
},
_locale: Locale({
  
_calendar: {
  
sameDay'[Today at] LT'nextDay'[Tomorrow at] LT',
nextWeek'dddd [at] LT'lastDay'[Yesterday at] LT',
lastWeek'[Last] dddd [at] LT'sameElse'L'
},
_longDateFormat: {
  
LTS'h:mm:ss A'LT'h:mm A'L'MM/DD/YYYY',
LL'MMMM D, YYYY'LLL'MMMM D, YYYY h:mm A',
LLLL'dddd, MMMM D, YYYY h:mm A'
},
_invalidDate'Invalid date',
ordinalfunction ordinal(number) {
  var b = number % 10,
    output = (toInt(number % 100 / 10) === 1) ? 'th' :
    (=== 1) ? 'st' :
    (=== 2) ? 'nd' :
    (=== 3) ? 'rd' : 'th';
  return number + output;
},
_ordinalParse/\d{1,2}(th|st|nd|rd)/,
_relativeTime: {
  
future'in %s'past'%s ago's'a few seconds',
m'a minute'mm'%d minutes'h'an hour',
hh'%d hours'd'a day'dd'%d days'M'a month',
MM'%d months'y'a year'yy'%d years'
},
_months: [
  
'January',
  
'February',
  
'March',
  
'April',
  
'May',
  
'June',
  
'July',
  
'August',
  
'September',
  
'October',
  
'November',
  
'December'
],
_monthsShort: [
  
'Jan',
  
'Feb',
  
'Mar',
  
'Apr',
  
'May',
  
'Jun',
  
'Jul',
  
'Aug',
  
'Sep',
  
'Oct',
  
'Nov',
  
'Dec'
],
_week: { dow0doy6 },
_weekdays: [
  
'Sunday',
  
'Monday',
  
'Tuesday',
  
'Wednesday',
  
'Thursday',
  
'Friday',
  
'Saturday'
],
_weekdaysMin: [ 'Su''Mo''Tu''We''Th''Fr''Sa' ],
_weekdaysShort: [ 'Sun''Mon''Tue''Wed''Thu''Fri''Sat' ],
_meridiemParse/[ap]\.?m?\.?/i_abbr'en',
_config: {
  
calendar: {
  
sameDay'[Today at] LT'nextDay'[Tomorrow at] LT',
nextWeek'dddd [at] LT'lastDay'[Yesterday at] LT',
lastWeek'[Last] dddd [at] LT'sameElse'L'
},
longDateFormat: {
  
LTS'h:mm:ss A'LT'h:mm A'L'MM/DD/YYYY',
LL'MMMM D, YYYY'LLL'MMMM D, YYYY h:mm A',
LLLL'dddd, MMMM D, YYYY h:mm A'
},
invalidDate'Invalid date',
ordinalfunction ordinal(number) {
  var b = number % 10,
    output = (toInt(number % 100 / 10) === 1) ? 'th' :
    (=== 1) ? 'st' :
    (=== 2) ? 'nd' :
    (=== 3) ? 'rd' : 'th';
  return number + output;
},
ordinalParse/\d{1,2}(th|st|nd|rd)/,
relativeTime: {
  
future'in %s'past'%s ago's'a few seconds',
m'a minute'mm'%d minutes'h'an hour',
hh'%d hours'd'a day'dd'%d days'M'a month',
MM'%d months'y'a year'yy'%d years'
},
months: [...], monthsShort: [...],
week: { dow0doy6 },
weekdays: [
  
'Sunday',
  
'Monday',
  
'Tuesday',
  
'Wednesday',
  
'Thursday',
  
'Friday',
  
'Saturday'
],
weekdaysMin: [ 'Su''Mo''Tu''We''Th''Fr''Sa' ],
weekdaysShort: [ 'Sun''Mon''Tue''Wed''Thu''Fri''Sat' ],
meridiemParse/[ap]\.?m?\.?/iabbr'en'
},
_ordinalParseLenient/\d{1,2}(th|st|nd|rd)|\d{1,2}/
}),
_dnew Date('Thu, 01 Jan 1970 00:00:01.337 GMT')
})
 
Moment({
  
_isAMomentObjecttrue,
  
_i
31337,
 
//
 
should equal 
1337
  
_isUTCfalse,
  
_pf: {
  
emptyfalseunusedTokens: [], unusedInput: [],
overflow-2charsLeftOver0nullInputfalse,
invalidMonthnullinvalidFormatfalse,
userInvalidatedfalseisofalse,
parsedDateParts: [], meridiemnull
},
  
_locale: Locale({
  
_calendar: {
  
sameDay'[Today at] LT'nextDay'[Tomorrow at] LT',
nextWeek'dddd [at] LT'lastDay'[Yesterday at] LT',
lastWeek'[Last] dddd [at] LT'sameElse'L'
},
_longDateFormat: {
  
LTS'h:mm:ss A'LT'h:mm A'L'MM/DD/YYYY',
LL'MMMM D, YYYY'LLL'MMMM D, YYYY h:mm A',
LLLL'dddd, MMMM D, YYYY h:mm A'
},
_invalidDate'Invalid date',
ordinalfunction ordinal(number) {
  var b = number % 10,
    output = (toInt(number % 100 / 10) === 1) ? 'th' :
    (=== 1) ? 'st' :
    (=== 2) ? 'nd' :
    (=== 3) ? 'rd' : 'th';
  return number + output;
},
_ordinalParse/\d{1,2}(th|st|nd|rd)/,
_relativeTime: {
  
future'in %s'past'%s ago's'a few seconds',
m'a minute'mm'%d minutes'h'an hour',
hh'%d hours'd'a day'dd'%d days'M'a month',
MM'%d months'y'a year'yy'%d years'
},
_months: [
  
'January',
  
'February',
  
'March',
  
'April',
  
'May',
  
'June',
  
'July',
  
'August',
  
'September',
  
'October',
  
'November',
  
'December'
],
_monthsShort: [
  
'Jan',
  
'Feb',
  
'Mar',
  
'Apr',
  
'May',
  
'Jun',
  
'Jul',
  
'Aug',
  
'Sep',
  
'Oct',
  
'Nov',
  
'Dec'
],
_week: { dow0doy6 },
_weekdays: [
  
'Sunday',
  
'Monday',
  
'Tuesday',
  
'Wednesday',
  
'Thursday',
  
'Friday',
  
'Saturday'
],
_weekdaysMin: [ 'Su''Mo''Tu''We''Th''Fr''Sa' ],
_weekdaysShort: [ 'Sun''Mon''Tue''Wed''Thu''Fri''Sat' ],
_meridiemParse/[ap]\.?m?\.?/i_abbr'en',
_config: {
  
calendar: {
  
sameDay'[Today at] LT'nextDay'[Tomorrow at] LT',
nextWeek'dddd [at] LT'lastDay'[Yesterday at] LT',
lastWeek'[Last] dddd [at] LT'sameElse'L'
},
longDateFormat: {
  
LTS'h:mm:ss A'LT'h:mm A'L'MM/DD/YYYY',
LL'MMMM D, YYYY'LLL'MMMM D, YYYY h:mm A',
LLLL'dddd, MMMM D, YYYY h:mm A'
},
invalidDate'Invalid date',
ordinalfunction ordinal(number) {
  var b = number % 10,
    output = (toInt(number % 100 / 10) === 1) ? 'th' :
    (=== 1) ? 'st' :
    (=== 2) ? 'nd' :
    (=== 3) ? 'rd' : 'th';
  return number + output;
},
ordinalParse/\d{1,2}(th|st|nd|rd)/,
relativeTime: {
  
future'in %s'past'%s ago's'a few seconds',
m'a minute'mm'%d minutes'h'an hour',
hh'%d hours'd'a day'dd'%d days'M'a month',
MM'%d months'y'a year'yy'%d years'
},
months: [
  
'January',
  
'February',
  
'March',
  
'April',
  
'May',
  
'June',
  
'July',
  
'August',
  
'September',
  
'October',
  
'November',
  
'December'
],
monthsShort: [
  
'Jan',
  
'Feb',
  
'Mar',
  
'Apr',
  
'May',
  
'Jun',
  
'Jul',
  
'Aug',
  
'Sep',
  
'Oct',
  
'Nov',
  
'Dec'
],
week: { dow0doy6 },
weekdays: [
  
'Sunday',
  
'Monday',
  
'Tuesday',
  
'Wednesday',
  
'Thursday',
  
'Friday',
  
'Saturday'
],
weekdaysMin: [ 'Su''Mo''Tu''We''Th''Fr''Sa' ],
weekdaysShort: [ 'Sun''Mon''Tue''Wed''Thu''Fri''Sat' ],
meridiemParse/[ap]\.?m?\.?/iabbr'en'
},
_ordinalParseLenient/\d{1,2}(th|st|nd|rd)|\d{1,2}/
}),
  
_d
new Date('Thu, 01 Jan 1970 00:00:31.337 GMT')
 
//
 
should equal 
new Date('Thu, 01 Jan 1970 00:00:01.337 GMT')
})
expect.addType({
  name: 'moment',
  base: 'object',
  identify: function (v) { return v && moment.isMoment(v); },
  inspect: function (v, depth, output) {
      output.jsFunctionName('moment').text('(')
            .jsPrimitive(v.toISOString()).text(')');
  },
  equal: function (a, b) { return a.isSame(b); },
  diff: function (actual, expected, output, diff) {
      return diff(actual.toISOString(), expected.toISOString());
  }
});
expect(moment(31337), 'to equal', moment(1337));
expected moment(1970-01-01T00:00:31.337Z)
to equal moment(1970-01-01T00:00:01.337Z)
 
1970-01-01T00:00:31.337Z
1970-01-01T00:00:01.337Z

Almost anything is possible

Create plugins that extends unexpected with new types, assertions, styles and themes.

unexpected-sinon

Testing with fakes

function Account(amount) {
  var that = this;
  that.deposit = function (amount) { /*...*/ };
  that.withdraw = function (amount) { /*...*/ };
  that.transferTo = function (amount, currency, destinationAccount) {
    that.withdraw(amount);
    destinationAccount.deposit(amount);
  };
}
var srcAccount = new Account();
var destAccount = new Account();
 
var sinon = require('sinon');
sinon.spy(srcAccount, 'withdraw');
sinon.spy(destAccount, 'deposit');
 
srcAccount.transferTo(250, 'dkk', destAccount);
expect.use(require('unexpected-sinon'));
 
expect([srcAccount.withdraw, destAccount.deposit],
       'to have calls satisfying', function () {
  srcAccount.withdraw({ amount: 250, currency: 'dkk' })
  destAccount.deposit({ amount: 250, currency: 'dkk' })
});
expected [ withdrawdeposit ] to have calls satisfying
withdraw( { amount250currency'dkk' } );
deposit( { amount250currency'dkk' } );
 
withdraw(
  
250 
//
 
should equal 
amount250currency'dkk' }
); at Account.that.transferTo (evalmachine.<anonymous>:6:10)
deposit(
  
250 
//
 
should equal 
amount250currency'dkk' }
); at Account.that.transferTo (evalmachine.<anonymous>:7:24)

Questions

The end

Stickers for everybody :-)