test-each
Version:
🤖 Repeat tests. Repeat tests. Repeat tests.
197 lines (186 loc) • 5.71 kB
TypeScript
/**
* Object whose properties can be used to generate test titles.
*/
export type Info = Readonly<{
/**
* Each combination of parameters is stringified as a `title`.
* Titles should be included in test titles to make them descriptive and
* unique.
*
* @example
* ```js
* each([{ color: 'red' }, { color: 'blue' }], ({ title }, param) => {
* // Test titles will be:
* // should test color | {"color": "red"}
* // should test color | {"color": "blue"}
* test(`should test color | ${title}`, () => {})
* })
*
* // Plain objects can override this using a `title` property
* each(
* [
* { color: 'red', title: 'Red' },
* { color: 'blue', title: 'Blue' },
* ],
* ({ title }, param) => {
* // Test titles will be:
* // should test color | Red
* // should test color | Blue
* test(`should test color | ${title}`, () => {})
* },
* )
*
* // The `info` argument can be used for dynamic titles
* each([{ color: 'red' }, { color: 'blue' }], (info, param) => {
* // Test titles will be:
* // should test color | 0 red
* // should test color | 1 blue
* test(`should test color | ${info.index} ${param.color}`, () => {})
* })
* ```
*/
title: string
/**
* Like `info.title` but for each input.
*/
titles: string[]
/**
* Incremented on each iteration. Starts at `0`.
*/
index: number
/**
* Index of each parameter inside each initial input.
*/
indices: number[]
}>
type UnknownFunction = (...args: never[]) => unknown
type InputArraysArgs = (unknown[] | number | UnknownFunction)[]
type CartesianProduct<InputArrays extends InputArraysArgs> = {
[index in keyof InputArrays]: Readonly<
InputArrays[index] extends (infer InputElement)[]
? InputElement
: InputArrays[index] extends number
? number
: InputArrays[index] extends InputFunction<InputArrays>
? ReturnType<InputArrays[index]>
: never
>
}
/**
* Input function used as input.
* Each iteration fires it and uses its return value.
* The function is called with the same arguments as the `callback`.
*
* @example
* ```js
* // Run callback with a different random number each time
* each(['red', 'green', 'blue'], Math.random, (info, color, randomNumber) => {})
*
* // Input functions are called with the same arguments as the callback
* each(
* ['02', '15', '30'],
* ['January', 'February', 'March'],
* ['1980', '1981'],
* (info, day, month, year) => `${day}/${month}/${year}`,
* (info, day, month, year, date) => {},
* )
* ```
*/
export type InputFunction<InputArrays extends InputArraysArgs = unknown[][]> = (
info: Info,
...args: InputFunctionArgs<InputArrays>
) => unknown
type InputFunctionArgs<InputArrays extends InputArraysArgs> = {
readonly [index in keyof InputArrays]: Readonly<
InputArrays[index] extends (infer InputElement)[]
? InputElement
: InputArrays[index] extends UnknownFunction
? InputArrays[index]
: InputArrays[index] extends number
? number
: never
>
}
/**
* Returns an
* [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#Iterables)
* looping through each combination of the inputs.
*
* @example
* ```js
* const combinations = iterable(
* ['green', 'red', 'blue'],
* [{ active: true }, { active: false }],
* )
*
* for (const [{ title }, color, param] of combinations) {
* test(`should test color | ${title}`, () => {})
* }
* ```
*/
export function iterable<InputArrays extends InputArraysArgs>(
...inputs: [...InputArrays]
): Generator<[Info, ...CartesianProduct<InputArrays>], void, void>
/**
* Fires `callback` with each combination of the inputs.
*
* @example
* ```js
* // The examples use Ava but any test runner works (Jest, Mocha, Jasmine, etc.)
* import test from 'ava'
*
* // The code we are testing
* import multiply from './multiply.js'
*
* // Repeat test using different inputs and expected outputs
* each(
* [
* { first: 2, second: 2, output: 4 },
* { first: 3, second: 3, output: 9 },
* ],
* ({ title }, { first, second, output }) => {
* // Test titles will be:
* // should multiply | {"first": 2, "second": 2, "output": 4}
* // should multiply | {"first": 3, "second": 3, "output": 9}
* test(`should multiply | ${title}`, (t) => {
* t.is(multiply(first, second), output)
* })
* },
* )
*
* // Snapshot testing. The `output` is automatically set on the first run,
* // then re-used in the next runs.
* each(
* [
* { first: 2, second: 2 },
* { first: 3, second: 3 },
* ],
* ({ title }, { first, second }) => {
* test(`should multiply outputs | ${title}`, (t) => {
* t.snapshot(multiply(first, second))
* })
* },
* )
*
* // Cartesian product.
* // Run this test 4 times using every possible combination of inputs
* each([0.5, 10], [2.5, 5], ({ title }, first, second) => {
* test(`should mix integers and floats | ${title}`, (t) => {
* t.is(typeof multiply(first, second), 'number')
* })
* })
*
* // Fuzz testing. Run this test 1000 times using different numbers.
* each(1000, Math.random, ({ title }, index, randomNumber) => {
* test(`should correctly multiply floats | ${title}`, (t) => {
* t.is(multiply(randomNumber, 1), randomNumber)
* })
* })
* ```
*/
export function each<InputArrays extends InputArraysArgs>(
...args: [
...inputs: [...InputArrays],
callback: (info: Info, ...params: CartesianProduct<InputArrays>) => void,
]
): void