Skip to content
/ gentest Public

Generative testing for JavaScript. Save time and catch more bugs by letting the computer write test cases for you. WIP

License

Notifications You must be signed in to change notification settings

graue/gentest

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gentest

Property-based, generative testing for JavaScript.

Don't handwrite unit tests. Save time and catch more bugs by writing properties, and let the computer generate test cases for you!

(This is a work in progress. Consider it "Stability 1: Experimental" for the time being. Feedback welcome.)

Basic example

Let's say we want to test this add function:

function add(x, y) {
  return x + y;
}

We can begin by asking, "What properties should this function have?" One property is that it's commutative; add(x, y) should equal add(y, x) for any integers x and y. To test this, we could write a function that accepts a particular pair of values for x and y, and returns true if the property holds for those inputs:

var ourProperty = function(x, y) {
  return add(x, y) === add(y, x);
};

Such a function is called a property in Gentest, but we're not quite finished. We also need to tell Gentest what x and y are so it can generate sample values. For now, let's restrict our input domain to integers, which we can create using the gentest.types.int generator.

var t = gentest.types;

forAll([t.int, t.int], 'addition is commutative', function(x, y) {
  return add(x, y) === add(y, x);
});

We now have a complete example and can run the tests using the gentest executable. npm install -g gentest, then run gentest with your test file as an argument.

Concepts and terms

A property is a parameterized test: a function that takes any number of arguments and returns a boolean, together with a description of how to generate that function's arguments.

A test is a particular test case, that is, a set of arguments to a property.

API

gentest.sample(type, [count])

Generates sample values of the given type.

gentest.types

Contains the following type definitions, with built-in generators:

  • int
  • int.nonNegative
  • int.nonZero
  • int.positive
  • char
  • string
  • bool
gentest.sample(gentest.types.int);
// -> [ 0, 0, -1, 1, 0, 2, -2, 1, -2, -4 ]

gentest.sample(gentest.types.string);
// -> [ '', '', '', 'V', 'N', '{C', '(P', 'jb', 'I{=y', 'Ss' ]

And these higher-order type definitions:

arrayOf(type)

Produces arrays of the argument type.

gentest.sample(gentest.types.arrayOf(gentest.types.bool));
// ->
// [ [],
//   [],
//   [ false ],
//   [ false ],
//   [ false ],
//   [ false ],
//   [ false, true, true ],
//   [ true, true, true ],
//   [ false,
//     false,
//     true,
//     true ],
//   [] ]

tuple(types)

Produces arrays that have one each of the given types, in order.

var t = gentest.types;
gentest.sample(t.tuple([t.int, t.int, t.bool, t.string]))
// ->
// [ [ -1, -1, true, '' ],
//   [ 0, 0, true, 'B' ],
//   [ 2, 1, true, '!B' ],
//   [ 0, 0, true, '' ],
//   [ 2, 2, false, '\'D' ],
//   [ 2, 2, true, '@+' ],
//   [ 3, 1, true, '7gR]' ],
//   [ -2, 0, true, 'Z' ],
//   [ 0, -4, false, 'rr$:' ],
//   [ 5, 4, true, '' ] ]

oneOf(types)

Produces any of the given types.

gentest.sample(gentest.types.oneOf([gentest.types.bool, gentest.types.int]));
// ->
// [ 0,
//   true,
//   1,
//   false,
//   true,
//   true,
//   true,
//   -1,
//   0,
//   -4 ]

constantly(x)

Returns a generator that always yields the constant value x.

elements(elems)

Any of the given elements.

var foods = gentest.types.elements(['pizza', 'chocolate', 'sushi']);
gentest.sample(foods);
// ->
// [ 'sushi',
//   'pizza',
//   'pizza',
//   'chocolate',
//   'sushi',
//   'pizza',
//   'chocolate',
//   'chocolate',
//   'chocolate',
//   'sushi' ]

shape(object)

Produces objects, with each key mapped to a value of the respective type.

var person = gentest.types.shape({
  name: gentest.types.string,
  age: gentest.types.int.positive
});
gentest.sample(person);
// ->
// [ { name: '', age: 1 },
//   { name: '', age: 1 },
//   { name: 'y', age: 1 },
//   { name: '$', age: 2 },
//   { name: 'v', age: 3 },
//   { name: '~', age: 2 },
//   { name: 'vA', age: 2 },
//   { name: 'u', age: 4 },
//   { name: 'QWb', age: 2 },
//   { name: '5,r', age: 3 } ]

fmap(fun, type)

Maps a function over the generated values of the given type.

var powersOfTwo = gentest.types.fmap(function(n) {
  return Math.pow(2, n);
}, gentest.types.int.nonNegative);

gentest.sample(powersOfTwo);
// -> [ 1, 1, 2, 2, 8, 4, 16, 32, 8, 2 ]

bind(type, fun)

A cousin of fmap where each generated value of type is mapped to a second generator, which is then sampled.

This allows you to combine generators in ways you couldn't with just fmap. For example, say you're testing a function similar to Array.prototype.indexOf, and you want arrays together with an element from the array:

var t = gentest.types;
function isNonempty(xs) { return xs.length > 0; }

// Helper: Generate non-empty arrays of ints.
var intArray = t.suchThat(
  isNonempty,
  t.arrayOf(t.int)
);

var arrayAndElement = t.bind(
  intArray,

  // This function takes an array *value*, generated by the inner
  // generator (intArray), and returns a *generator*: in this case,
  // of elements selected from the array, paired with the array
  // itself.
  function(ints) {
    return t.tuple([t.elements(ints), t.constantly(ints)]);
  }
);

gentest.sample(arrayAndElement);
// ->
// [ [ -1, [ -1 ] ],
//   [ -2, [ -2, 2 ] ],
//   [ -2, [ -3, -2, 2 ] ],
//   [  1, [ -3, 1 ] ],
//   [ -4, [ -4, -1, 2, -4 ] ],
//   [  2, [ 2 ] ],
//   [  4, [ 4, -5 ] ],
//   [ -2, [ 1, -2, -4, -1, -4, -2 ] ],
//   [  3, [ -4, 6, -5, 6, 3 ] ],
//   [ -6, [ -6, 1, 6, -3, -6 ] ] ]

suchThat(pred, type, [maxTries])

Produces values of type that pass the predicate pred. This should be a predicate that will pass most of the time; you can't use this to select for relatively rare values like prime numbers, perfect squares, strings with balanced parentheses, etc.

A common use case is non-empty arrays:

function isNonempty(xs) { return xs.length > 0; }

var nonemptyArray = t.suchThat(
  isNonempty,
  t.arrayOf(t.int)
);

If you can, it's better to generate the values you want directly instead of filtering for them. For example, this is a not-so-great way to generate multiples of 3:

var threesBad = t.suchThat(
  function(n) { return n%3 === 0; },
  t.int.nonNegative);

This is a better way, more reliable and efficient:

var threesGood = t.fmap(
  function(n) { return n*3; },
  t.int.nonNegative);

Writing your own generators

A design goal of Gentest is that you as a user should never have to write your own generators from scratch. Instead, everything you need to test should be expressible in terms of the primitives above and the higher-order generators like fmap and bind.

By doing it this way you get shrinking and repeatability of test cases automatically for your new types.

How does this work in practice? Let's say you have a rectangle class which contains x and y coordinates, a width, a height, and a method to test if it's colliding with another rectangle:

var Rect = function(x, y, w, h) {
  this.x = x;
  this.y = y;
  this.w = w;
  this.h = h;
}
Rect.prototype.isColliding = function(other) { /* ... */ };

To make a Rect, you essentially just need to create x, y, width and height values and pass them to the constructor. You can generate the values using tuple:

t.tuple([t.int,          t.int,          // x, y
         t.int.positive, t.int.positive  // width, height. We don't want
                                         // these to be 0 or negative!
        ]);

Then map a function over each generated value:

var genRect =
  t.fmap(
    function(tuple) {
      return new Rect(tuple[0], tuple[1], tuple[2], tuple[3]);
    },
    t.tuple([t.int,          t.int,
             t.int.positive, t.int.positive]));

And now use genRect in your properties just like a built-in type:

forAll([genRect], 'rectangles collide with themselves', function(rect) {
  return rect.isColliding(rect);
});

Avoid calling Math.random in your functions, since if you do so, test runs won't be repeatable. All randomness should come from the built-in generators.

If the generator you want seems impossible to write, check the issues because something may be missing. And feel free to ask for help. But in general, with fmap and bind you have a lot of power to build more sophisticated generators.

Credits

gentest is heavily influenced by QuickCheck and test.check.

About

Generative testing for JavaScript. Save time and catch more bugs by letting the computer write test cases for you. WIP

Resources

License

Stars

Watchers

Forks

Packages

No packages published