to be valid for all

  • <function> to be valid for all <chance-generator+>
  • <function> to be valid for all <object>

An assertion that runs the subject function up to 300 times with different generated input. If the subject function fails an error is thrown.

Let's tests that lodash.unescape reverses lodash.escape we do that the following way:

const escape = require('lodash.escape');
const unescape = require('lodash.unescape');
const { string } = require('chance-generators');
 
expect(
  (text) => {
    expect(unescape(escape(text)), 'to equal', text);
  },
  'to be valid for all',
  string({ max: 200 })
);

This will run 300 tests with random strings of length 0-200 and succeed.

You can specify the max number of iterations that the test should run and the number of errors it should collect before stopping.

The algorithm searches for the smallest error output, so the more errors you allow it to collect the better the output will be.

expect(
  (text) => {
    expect(unescape(escape(text)), 'to equal', text);
  },
  'to be valid for all',
  {
    generators: [string({ max: 200 })],
    maxIterations: 1000,
    maxErrors: 30,
  }
);

I found to following code for Run-length encoding on the internet, let's see if that code also fulfill our round trip test:

function rleEncode(input) {
  var encoding = [];
  input.match(/(.)\1*/g).forEach((substr) => {
    encoding.push([substr.length, substr[0]]);
  });
  return encoding;
}
 
function rleDecode(encoded) {
  var output = '';
  encoded.forEach((pair) => {
    output += new Array(1 + pair[0]).join(pair[1]);
  });
  return output;
}
 
expect(
  (text) => {
    expect(rleDecode(rleEncode(text)), 'to equal', text);
  },
  'to be valid for all',
  string({ max: 200 })
);
Found an error after 220 iterations
counterexample:
 
  
Generated input: ''
with: string({ min0max200 })
 
TypeError: Cannot read properties of null (reading 'forEach')
    at rleEncode (evalmachine.<anonymous>:3:25)
    at string.max (evalmachine.<anonymous>:18:20)
    at /home/andreas/work/unexpected-check/lib/unexpected-check.js:262:36
    at /home/andreas/work/unexpected-check/node_modules/unexpected/build/lib/makePromise.js:68:32
    at tryCatcher (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/util.js:26:23)
    at Promise._resolveFromResolver (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/promise.js:476:31)
    at new Promise (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/promise.js:69:37)
    at Function.makePromise [as promise] (/home/andreas/work/unexpected-check/node_modules/unexpected/build/lib/makePromise.js:17:10)
    at /home/andreas/work/unexpected-check/lib/unexpected-check.js:261:20
    at loop (/home/andreas/work/unexpected-check/lib/unexpected-check.js:168:20)
    at tryCatcher (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/util.js:26:23)
    at Promise._settlePromiseFromHandler (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/promise.js:503:31)
    at Promise._settlePromiseAt (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/promise.js:577:18)
    at Promise._settlePromises (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/promise.js:693:14)
    at Async._drainQueue (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/async.js:123:16)
    at Async._drainQueues (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/async.js:133:10)
    at Async.drainQueues (/home/andreas/work/unexpected-check/node_modules/unexpected-bluebird/js/main/async.js:15:14)
    at /home/andreas/work/unexpected-check/node_modules/unexpected/build/lib/workQueue.js:7:7
    at Array.forEach (<anonymous>)
    at Object.drain (/home/andreas/work/unexpected-check/node_modules/unexpected/build/lib/workQueue.js:6:16)
    at oathbreaker (/home/andreas/work/unexpected-check/node_modules/unexpected/build/lib/oathbreaker.js:44:13)
    at Function.expectPrototype._executeExpect (/home/andreas/work/unexpected-check/node_modules/unexpected/build/lib/createTopLevelExpect.js:1517:10)

Something is failing for the empty string input. The problem is that the regular expression in the encoder does not match the empty string. This would probably also have been found in a unit test, but these edge cases are easily found using property based testing. Imagine more complex scenarios where code only fails for the null character, these cases might be harder to come up with while doing normal unit testing.

You can supply as many generators as you want. My examples are using chance-generators but you can using any function that produces a random output when called.

Here is a test that uses more than one generator:

const { word } = require('chance-generators');
 
expect(
  (a, b) => {
    return (+ b).length === a.length + b.length;
  },
  'to be valid for all',
  word,
  word
);

Another example could be to generate actions.

Let's create a simple queue:

function Queue() {
  this.buffer = [];
}
Queue.prototype.enqueue = function (value) {
  this.buffer.push(value);
};
Queue.prototype.dequeue = function () {
  return this.buffer.shift(1);
};
Queue.prototype.isEmpty = function () {
  return this.buffer.length === 0;
};
Queue.prototype.drainTo = function (array) {
  while (!this.isEmpty()) {
    array.push(this.dequeue());
  }
};

Now let's test that items enqueued always comes out in the right order:

const { array, pickone } = require('chance-generators');
 
var action = pickone([{ name: 'enqueue', value: string }, { name: 'dequeue' }]);
 
var actions = array(action, 200);
 
expect(
  function (actions) {
    var queue = new Queue();
    var enqueued = [];
    var dequeued = [];
    actions.forEach(function (action) {
      if (action.name === 'enqueue') {
        enqueued.push(action.value);
        queue.enqueue(action.value);
      } else if (!queue.isEmpty()) {
        dequeued.push(queue.dequeue());
      }
    });
 
    queue.drainTo(dequeued);
 
    expect(dequeued, 'to equal', enqueued);
  },
  'to be valid for all',
  actions
);

Support for asynchronous testing by returning a promise from the subject function:

expect.use(require('unexpected-stream'));
 
return expect(
  function (text) {
    return expect(
      text,
      'when piped through',
      [require('zlib').Gzip(), require('zlib').Gunzip()],
      'to yield output satisfying',
      'when decoded as',
      'utf-8',
      'to equal',
      text
    );
  },
  'to be valid for all',
  string
);