Skip to content

Property-based testing library. Inspired by principled type classes.

License

Notifications You must be signed in to change notification settings

veigaribo/lawful-typeclasses

Repository files navigation

Lawful Type Classes

lawful-typeclasses is a library designed to provide a way of asserting the behavior of your JavaScript classes.

"Lawful" here refers to a characteristic of principled type classes.

What it does

This library allows you to define two things: classes and instances. Perhaps a bit confusedly, classes are JavaScript objects and instances are JavaScript classes.

We'll be referring to the JavaScript classes that implement the behavior of a type class (and are thus instances of that class) as constructors and to the instances of those JavaScript classes as instance values.

What this library then allows you to do is to check if every constructor follows the rules defined in their classes, so that you are able to modularize your tests in a neat way.

Classes

A class is what defines the behavior that you want your instances to conform to.

For example, let's say that you want to define a class of things that can be added:

// We use a builder because it allows for type inferences that would not be
// possible with simple parameters.

// The class is named "Addable". this will be used in error messages.
const addable = new ClassBuilder('Addable')
  // In this example, we are using TypeScript, so we need to specify the type
  // of the objects we want to test. This should be one or more interfaces, for
  // composability. If, and only if you are not using static typing, this may
  // be omitted.
  .withType<Addable & Eq>()
  // Next, we define the properties we expect our instances to have.
  // We'll start out by using the `all` function to say that, in order to
  // be an Addable, the constructor must obey all of the following laws
  // (not just any).
  .withLaws(
    all(
      // Using named functions is not necessary, but it helps to improve error
      // messages.
      // Each parameter to an `obey` function will be a value generated by the
      // Generator, which we will go over shortly. Your function may ask for as
      // many as it wants, and the system will take care of providing them.
      obey(function commutativity(x, y) {
        // `x` and `y` will have type `Addable & Eq`, as we defined above.
        const a = x.add(y)
        const b = y.add(x)

        return a.equals(b)
      }),
      obey(function associativity(x, y, z) {
        const a = x.add(y.add(z))
        const b = x.add(y).add(z)

        return a.equals(b)
      }),
    ),
  )
  .build()

But, as you might have seen, we also expect our instances to implement an #equals method.

We could make use of another class:

const eq = new ClassBuilder('Eq')
  .withType<Eq>()
  .withLaws(
    obey(function reflexivity(x) {
      return x.equals(x)
    }),
  )
  .build()

And then the Addable class may extend Eq, meaning that, in order to be an instance of Addable, the constructor must also be an instance of Eq:

const addable = new ClassBuilder('Addable')
  // We don't need to specify the types of parent classes. They will be inferred.
  .withType<Addable>()
  .withParents(eq)
  // Laws will expect `Addable & Eq` just the same.
  .withLaws(/* ... */)
  .build()

Instances

Instances are JavaScript sets of values that behave according to some (type) class.

Let's start with the following:

class Number {
  constructor(public readonly n: number) {}

  equals(other: Number): boolean {
    return this.n === other.n
  }

  add(other: Number): Number {
    return new Number(this.n + other.n)
  }
}

In order to declare it as an instance of something, you must provide a way of generating values from it. These are the values that will be used for testing. (See How it works)

There are two ways of doing that:

// You may ask for as many parameters as you want, and to each one will be
// assigned a random number between 0 and 1 (inclusive).
// From these numbers you may generate an instance of your constructor.
// The name is used for error reporting.
const gen = continuous('Number', (n) => new Number(n))

// Note that, to increase the likelihood of catching edge cases, sometimes the
// generated numbers will be all 0s or 1s.
// Testing values will be sampled from the given array.
const gen = discrete('Number', [new Number(0), new Number(1), new Number(2)])

// This method would be more useful if we had a finite number of possible
// values, which is not the case.

And then you only need to call instance with the correct parameters and the validators will run. You should call this at some point in your tests.

// will throw an Error if it fails
instance(addable, gen)

Additionally, you may specify how many times each law will be tested (The default is 15 times):

instance(addable, gen, { sampleSize: 10 })

How it works

When instance is called, a sample of random instance values will be created using your provided generator, and each class property will be tested using those. If any of the laws fails to be asserted, an error is thrown, and you may be sure that the constructor in question is not an instance of the class you declared.

In case it passes, you may have a high confidence that it is.

About

Property-based testing library. Inspired by principled type classes.

Resources

License

Stars

Watchers

Forks

Packages

No packages published