Skip to content

rweda/configurable-arbitrary

Repository files navigation

ConfigurableArbitrary

Configurable. Extendable. Modular. Concise.

ConfigurableArbitrary helps you write intermediate and final data structures for JSVerify, a powerful QuickCheck library for Node.JS.

Background

JSVerify has a built-in type Arbitrary, which can generate stimulus for testing. JSVerify ships with arbitraries for primitive data types (string, number, etc.) and you can create your own generators.

However, JSVerify arbitraries can't be configured. Instead, JSVerify has seperate arbitraries for different use cases, including nestring to ensure the string has contents, and asciistring to ensure that characters can only come from the ASCII character set.

Shipping different arbitraries for primitive types works, but becomes impossible when dealing with larger data types. That's where ConfigurableArbitrary steps in.

Basic Usage Guide

ConfigurableArbitrary is base class that goes from run-time configuration to an Arbitrary, intended for creating object instances and other complex data types.

This process is broken into three stages to allow flexibility through stage-specific configuration or stages to be fully overridden.

Users should extend the ConfigurableArbitrary class to provide properties or replace classes. ConfigurableArbitrary comes with methods for each stage, starting with ConfigurableArbitrary.build. The class also contains internal methods to help some of the stages, and some basic utilities that simplify option definitions.

For this guide, we'll be creating a URLArbitrary, which can generate URL-like strings for testing purposes.

const ConfigurableArbitrary = require("configurable-arbitrary");

class URLArbitrary extends ConfigurableArbitrary {
  /* Properties and methods will be defined later in the documentation. */
}

module.exports = URLArbitrary;

Then in testing, we'll be able to use the arbitrary. First, we'll bake configuration options into the Arbitrary, converting the ConfigurableArbitrary into a a JSVerify Arbitrary.

const jsc = require("jsverify");
const URLArbitrary = require("./URLArbitrary.js");

let url = URLArbitrary.build({
  domain: jsc.constant("google.com"),
});

The configuration options are described in more depth later, but the domain will be merged with the protocol (e.g. https, etc.) and path to create the full URL string. We aren't specifying the protocol or path, so they will still be default values.

url is now a valid JSVerify Arbitrary. We can pass the Arbitrary to utilities like jsc.forall to run tests:

jsc.assert(jsc.forall(url, ({url}) => {
  return url.indexOf("://") > -1;
}));

Stage 1: Configuration

Out of the box, ConfigurableArbitrary comes without any configuration options. However, the class is setup to parse any options you'd like to define.

By default, these options won't be used in the final output. They are just to provide options to the actual generator.

The first stage involves merging option defaults from the extended class (and any other class between the final class and ConfigurableArbitrary), and merging these with the user-defined options.

Each sub-class of ConfigurableArbitrary can define an opts property that will be merged into the run-time configuration:

class URLArbitrary extends ConfigurableArbitrary {
  static get opts() {
    return {
      
      protocols: [ "http://", "https://" ],
      
      protocol: null,
      
      domain: null,
      
      path: null,
      
      title: null,
      
    };
  }
}

Stage 2: Arbitrary Specification

Because ConfigurableArbitrary was created with objects and complex data types in mind, it assumes that most users will want multiple Arbitrary values to configure.

Even for our simple URLArbitrary above, which will just output a single string and the page title, the configuration is split into several chunks so that users can override specific sections of the URL while leaving the other sections unchanged.

Each sub-class of ConfigurableArbitrary can define a spec method, which will be merged into an object that is passed to jsverify.record, which will merge them into an object so they can be passed as a single variable.

The spec object will be pre-populated with any options that already are Arbitrary objects.

const jsc = require("jsverify");

class URLArbitrary extends ConfigurableArbitrary {
  static spec(opts) {
    return {
      
      protocol: protocol => this.defaultArbitrary(protocol, () => this.protocol(opts.protocols)),
      
      domain: domain => this.defaultArbitrary(domain, jsc.nestring),
      
      path: path => this.defaultArbitrary(path, jsc.string),
      
      title: title => this.defaultArbitrary(title, jsc.string),
      
    };
  }
  
  static protocol(protocols) {
    return jsc.oneof(protocols.map(protocol => jsc.constant(protocol)));
  }
}

This example uses ConfigurableArbitrary.defaultArbitrary, which uses previously-defined arbitraries (such as arbitraries coming from run-time options) if they exist, otherwise it uses the defined arbitrary for this stage.

Stage 3: Transformations

ConfigurableArbitrary lets you define a transform method that is passed the Arbitrary created by jsc.record() in the previous step. By default, ConfigurableArbitrary uses a no-op/identity function that does not apply any transformation.

Unlike opts and spec which traverse the inheritance tree and merge all intermediate classes, transform is only called on the final step, and you must manually call super to get the result from intermediate classes.

class URLArbitrary extends ConfigurableArbitrary {
  static transform(arb) {
    return this.smapobj(arb, (opts) => {
      return {
        url: `${opts.protocol}${opts.domain}${opts.path}`,
        title: opts.title,
      };
    });
  }
}

This example uses ConfigurableArbitrary.smapobj, which automatically preforms a two-way mapping which is needed for jsverify.

Usage of URLArbitrary

Now that we've defined the URLArbitrary, we can use it in testing.

const jsc = require("jsverify");
jsc.ava = require("ava-verify");

const arb = URLArbitrary.build({
  path: jsc.constant("/"),
  title: jsc.constant("Google"),
});

jsc.ava({
  suite: "Creates URLs",
}, [ arb ], (t, url) => {
  t.not(url.url.indexOf("://"), -1);
});

This test is running via our ava-verify plugin for the AVA test runner, but the arbitrary will work in any JSVerify test platform.