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