Skip to content

Commit 94aaa76

Browse files
committed
Improve data-driven testing
1 parent 4b6f05c commit 94aaa76

File tree

20 files changed

+302
-233
lines changed

20 files changed

+302
-233
lines changed

test/colors.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'use strict'
22

3+
const test = require('ava')
34
const hasAnsi = require('has-ansi')
45
const supportsColor = require('supports-color')
56

6-
const { forEachEvent, startLogging } = require('./helpers')
7+
const { repeatEvents, startLogging } = require('./helpers')
78

89
/* eslint-disable max-nested-callbacks */
9-
forEachEvent(({ eventName, emitEvent, test }) => {
10-
test('should colorize default opts.getMessage()', async t => {
10+
repeatEvents((prefix, { eventName, emitEvent }) => {
11+
test(`${prefix} should colorize default opts.getMessage()`, async t => {
1112
const { stopLogging, log } = startLogging({ log: 'spy', eventName })
1213

1314
await emitEvent()
@@ -18,7 +19,7 @@ forEachEvent(({ eventName, emitEvent, test }) => {
1819
stopLogging()
1920
})
2021

21-
test('should allow forcing colorizing default opts.getMessage()', async t => {
22+
test(`${prefix} should allow forcing colorizing default opts.getMessage()`, async t => {
2223
const { stopLogging, log } = startLogging({
2324
log: 'spy',
2425
colors: true,
@@ -33,7 +34,7 @@ forEachEvent(({ eventName, emitEvent, test }) => {
3334
stopLogging()
3435
})
3536

36-
test('should allow disabling colorizing default opts.getMessage()', async t => {
37+
test(`${prefix} should allow disabling colorizing default opts.getMessage()`, async t => {
3738
const { stopLogging, log } = startLogging({
3839
log: 'spy',
3940
colors: false,

test/emit.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const {
66
ALL_EVENTS: { all },
77
} = require('../helpers')
88

9-
const { forEachEvent, startLogging } = require('./helpers')
9+
const { repeatEvents, startLogging } = require('./helpers')
1010

1111
test('[all] events emitters should not throw', async t => {
1212
const { stopLogging } = startLogging()
@@ -17,13 +17,12 @@ test('[all] events emitters should not throw', async t => {
1717
})
1818

1919
/* eslint-disable max-nested-callbacks */
20-
// eslint-disable-next-line no-shadow
21-
forEachEvent(({ emitEvent, test }) => {
22-
test('events emitters should exist', t => {
20+
repeatEvents((prefix, { emitEvent }) => {
21+
test(`${prefix} events emitters should exist`, t => {
2322
t.is(typeof emitEvent, 'function')
2423
})
2524

26-
test('events emitters should not throw', async t => {
25+
test(`${prefix} events emitters should not throw`, async t => {
2726
const { stopLogging } = startLogging()
2827

2928
await t.notThrowsAsync(emitEvent)

test/exit.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
const process = require('process')
44

5+
const test = require('ava')
56
const sinon = require('sinon')
67
const lolex = require('lolex')
78

89
// eslint-disable-next-line import/no-internal-modules
910
const { EXIT_TIMEOUT } = require('../src/exit')
1011

11-
const { forEachEvent, startLogging } = require('./helpers')
12+
const { repeatEvents, startLogging } = require('./helpers')
1213

1314
// Stub `process.exit()`
1415
const stubProcessExit = function() {
@@ -28,8 +29,8 @@ const emitEventAndWait = async function(timeout, { clock, emitEvent }) {
2829
}
2930

3031
/* eslint-disable max-nested-callbacks */
31-
forEachEvent(({ eventName, emitEvent, test }) => {
32-
test('should call process.exit(1) if inside opts.exitOn', async t => {
32+
repeatEvents((prefix, { eventName, emitEvent }) => {
33+
test(`${prefix} should call process.exit(1) if inside opts.exitOn`, async t => {
3334
const { clock, processExit } = stubProcessExit()
3435

3536
const exitOn = [eventName]
@@ -45,7 +46,7 @@ forEachEvent(({ eventName, emitEvent, test }) => {
4546
unstubProcessExit({ clock, processExit })
4647
})
4748

48-
test('should not call process.exit(1) if not inside opts.exitOn', async t => {
49+
test(`${prefix} should not call process.exit(1) if not inside opts.exitOn`, async t => {
4950
const { clock, processExit } = stubProcessExit()
5051

5152
const exitOn = []
@@ -60,7 +61,7 @@ forEachEvent(({ eventName, emitEvent, test }) => {
6061
unstubProcessExit({ clock, processExit })
6162
})
6263

63-
test('should delay process.exit(1)', async t => {
64+
test(`${prefix} should delay process.exit(1)`, async t => {
6465
const { clock, processExit } = stubProcessExit()
6566

6667
const { stopLogging } = startLogging({ exitOn: [eventName], eventName })

test/helpers/data_driven.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

test/helpers/data_driven/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict'
2+
3+
module.exports = {
4+
...require('./main'),
5+
}

test/helpers/data_driven/main.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict'
2+
3+
const { getPrefixes } = require('./prefix')
4+
const { cartesianProduct } = require('./utils')
5+
6+
// Repeat a function with a combination of arguments.
7+
// Meant for test-driven development.
8+
// One of the design goals is to be separate from the test framework:
9+
// - this can be applied to any test library, not just ava
10+
// - this does not require patching test framework functions, which usually
11+
// gets in the way of linting such as `eslint-plugin-ava`
12+
// TODO: extract this library
13+
// - check other data-driven test libraries for features
14+
// - allow values to be generating functions
15+
const repeat = function(...args) {
16+
const arrays = args.slice(0, -1)
17+
const func = args[args.length - 1]
18+
19+
const arraysA = cartesianProduct(...arrays)
20+
21+
const prefixes = getPrefixes(arraysA)
22+
23+
return arraysA.map((values, index) => func(prefixes[index], ...values))
24+
}
25+
26+
module.exports = {
27+
repeat,
28+
}

test/helpers/data_driven/prefix.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict'
2+
3+
const { hasDuplicates } = require('./utils')
4+
5+
// Retrieve unique prefix for each combination of values
6+
const getPrefixes = function(arrays) {
7+
const prefixes = arrays.map(getPrefix)
8+
const prefixesA = fixDuplicates({ prefixes })
9+
return prefixesA
10+
}
11+
12+
const getPrefix = function(values) {
13+
return values
14+
.map(getValuePrefix)
15+
.filter(Boolean)
16+
.join(' ')
17+
}
18+
19+
const getValuePrefix = function(value) {
20+
const valueA = serializeValue(value)
21+
22+
if (valueA === undefined) {
23+
return
24+
}
25+
26+
const valueB = valueA.slice(0, MAX_PREFIX_LENGTH)
27+
return `[${valueB}]`
28+
}
29+
30+
const MAX_PREFIX_LENGTH = 20
31+
32+
// Try to serialize value.
33+
// Objects must have a `name` member.
34+
const serializeValue = function(value) {
35+
if (value === null || typeof value !== 'object') {
36+
return String(value)
37+
}
38+
39+
if (typeof value.name === 'string') {
40+
return value.name
41+
}
42+
}
43+
44+
// Add an incrementing counter if some prefixes are duplicates
45+
// TODO: use an incrementing counter for each value instead of for all values
46+
// at once
47+
const fixDuplicates = function({ prefixes }) {
48+
if (!hasDuplicates(prefixes)) {
49+
return prefixes
50+
}
51+
52+
return prefixes.map(addPrefixIndex)
53+
}
54+
55+
const addPrefixIndex = function(prefix, index) {
56+
const space = prefix.length === 0 ? '' : ' '
57+
return `[${index}]${space}${prefix}`
58+
}
59+
60+
module.exports = {
61+
getPrefixes,
62+
}

test/helpers/data_driven/utils.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict'
2+
3+
// Does a cartesian product on several arrays
4+
const cartesianProduct = function(combs, array, ...arrays) {
5+
const combsA = combs.map(comb => (Array.isArray(comb) ? comb : [comb]))
6+
7+
if (!Array.isArray(array)) {
8+
return combsA
9+
}
10+
11+
const combsB = combsA.flatMap(comb => arrayProduct({ comb, array }))
12+
return cartesianProduct(combsB, ...arrays)
13+
}
14+
15+
const arrayProduct = function({ comb, array }) {
16+
return array.map(value => [...comb, value])
17+
}
18+
19+
// Check if an array has duplicate items
20+
const hasDuplicates = function(array) {
21+
return array.some((valueA, index) => isDuplicate({ array, valueA, index }))
22+
}
23+
24+
const isDuplicate = function({ array, valueA, index }) {
25+
return array.slice(index + 1).some(valueB => valueB === valueA)
26+
}
27+
28+
module.exports = {
29+
cartesianProduct,
30+
hasDuplicates,
31+
}

test/helpers/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ module.exports = {
66
...require('./stack'),
77
...require('./several'),
88
...require('./data_driven'),
9+
...require('./repeat'),
910
}

test/helpers/repeat.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict'
2+
3+
// eslint-disable-next-line import/no-internal-modules
4+
const { LEVELS } = require('../../src/level')
5+
const { EVENTS } = require('../../helpers')
6+
7+
const { repeat } = require('./data_driven')
8+
9+
const getEvents = function() {
10+
return Object.entries(EVENTS).map(([eventName, emitEvent]) => ({
11+
eventName,
12+
emitEvent,
13+
name: eventName,
14+
}))
15+
}
16+
17+
const getLevels = function() {
18+
return Object.keys(LEVELS)
19+
}
20+
21+
const repeatEvents = repeat.bind(null, getEvents())
22+
const repeatLevels = repeat.bind(null, getLevels())
23+
const repeatEventsLevels = repeat.bind(null, getEvents(), getLevels())
24+
25+
module.exports = {
26+
repeatEvents,
27+
repeatLevels,
28+
repeatEventsLevels,
29+
}

0 commit comments

Comments
 (0)