Skip to main content

Fuzz Testing

General Use

Leveraging the technology from the amazing fast-check npm package, Pineapple enables you to write rather comprehensive tests in a small, single statements.

/**
* Adds two numbers together
* @test #integer, #integer returns @ as number
* @test #integer, #string throws
* @test #string, #integer throws
* @param {number} a
* @param {number} b
*/
export function add (a, b) {
if (typeof a !== 'number' || typeof b !== 'number') throw new Error('Not numbers')
return a + b
}

By using a #arbitrary tag, you're able to describe what information should be tested, Pineapple + Fast-Check will try a handful of test cases against your function and try to find any counter-examples where your condition fails.

This technology also works with snapshots.

You may also invoke the #arbitrary tags with arguments, as if it were a function call, and construct objects / tuples from them. You may also use args to refer to the arguments that were used to call the function, which is particularly useful for fuzzed-test cases.

/*
* @test { name: #string, age: #integer(1, 20) } throws
* @test { name: #string, age: #integer(21, 80) } returns cat(args.0.name, ' is drinking age.')
*/
export function drinkingAge ({ name, age }) {
if (age >= 21) return `${name} is drinking age.`
throw new Error(`${name} is not drinking age.`)
}

If a counter-example is found, fast-check will use a shrinking algorithm to find the smallest possible test-case to trigger the error, for example, if you wrote a sum function like so:

/**
* A simple template function.
* @test 'Hello $0' ~> #string returns cat('Hello ', args.0)
* @param {string} templateString
*/
export function template (templateString) {
/** @param {string} replace */
return replace => templateString.replace(/\$0/g, replace)
}

It would generate the following:

✖ Failed test (template): 'Hello $0' ~> #string returns cat('Hello ', args.0)
>> file:///Users/jesse/Documents/Projects/pineapple/test/fuzz.js:35
- Expected
+ Received

- Hello $$
+ Hello $
Failing Example: [
"$$"
]
Shrunk 4 times.
Seed: -2121637705

Which should help isolate the nature of the issue (in this case, "$" is special in the replace function, therefore it needs to be escaped with another dollar sign). In this case, the shrinking helps deduce the nature of the issue & reproduce it easily.

You may see a list of all built-in arbitraries here.

Applying Operations to an Arbitrary

In some cases, you may wish to apply operations to the fuzzed value, Pineapple will parse & handle this use case.

/**
* @test #integer(1, 10000) ** 2 returns true
*/
export function isSquare (num) {
return Math.sqrt(num) % 1 === 0
}

Adding new Arbitraries

If you wish to generate complex data structures, or need to introduce a new type of data-set to fuzz against, you are able to do so by implementing a new arbitrary type, and returning it from a function tagged with pineapple_define.

You may read up on how to implement them under fast-check's documentation.

import fc from 'fast-check' 

/**
* @pineapple_define
*/
function arbitraries () {
return {
// you can pass in an arbitrary, a function that returns an arbitrary, or a value that'll be constant.
// it will be named the value that you define here in the Pineapple engine.
person: fc.record({ id: fc.integer(), name: fc.string(), age: fc.integer(12, 80) })
}
}

/**
* @test #person returns args.0.age > 21
*/
function ofAge (person) {
return person.age > 21
}

It is also possible to provide namespaces for your arbitraries:

/**
* @pineapple_define tavern
*/
function arbitraries () {
return {
// you can pass in an arbitrary, a function that returns an arbitrary, or a value that'll be constant.
// it will be named the value that you define here in the Pineapple engine.
person: fc.record({ id: fc.integer(), name: fc.string(), age: fc.integer(12, 80) })
}
}

/**
* @test #tavern.person returns args.0.age > 21
*/
function ofAge (person) {
return person.age > 21
}

This should make it simpler to organize some of your static / generated data sets that you would like to use for your tests.