From 2bc6d945072cf2805c75858e1def9c1fd6adc717 Mon Sep 17 00:00:00 2001 From: Marc Cataford Date: Fri, 7 Apr 2023 23:14:15 -0400 Subject: [PATCH] feat: each support (#13) * feat: add each support for test, describe * docs: document test * feat: label templating basic support * refactor: group test exports together * refactor: leverage each in repetitive tests * chore: pinned node -> alias * test: coverage for it, test --- .nvmrc | 2 +- src/index.ts | 3 ++ src/testCaseUtils.ts | 36 -------------- src/testComponents/describe.ts | 43 ++++++++++++++++ src/{ => testComponents}/expect.ts | 2 +- src/{ => testComponents}/matchers.ts | 2 +- src/testComponents/test.ts | 53 ++++++++++++++++++++ src/types.ts | 4 +- tests/expect.test.ts | 74 ++++++++++------------------ tests/it.test.ts | 14 ++++++ 10 files changed, 145 insertions(+), 88 deletions(-) create mode 100644 src/index.ts delete mode 100644 src/testCaseUtils.ts create mode 100644 src/testComponents/describe.ts rename src/{ => testComponents}/expect.ts (99%) rename src/{ => testComponents}/matchers.ts (97%) create mode 100644 src/testComponents/test.ts create mode 100644 tests/it.test.ts diff --git a/.nvmrc b/.nvmrc index 55bffd6..a77793e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.15.0 +lts/hydrogen diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c8ba9c2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export { default as test, it } from './testComponents/test' +export { default as describe } from './testComponents/describe' +export { default as expect } from './testComponents/expect' diff --git a/src/testCaseUtils.ts b/src/testCaseUtils.ts deleted file mode 100644 index a5e6f84..0000000 --- a/src/testCaseUtils.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { promises as fs } from 'fs' - -import expect from './expect' -import { greenText, redText } from './utils' -import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from './types' - -function describe(label: TestCaseLabel, testGroup: TestCaseGroup) { - if (process.env.COLLECT) { - testGroup() - return - } - - console.group(greenText(label)) - testGroup() - console.groupEnd() -} - -function test(label: TestCaseLabel, testCase: TestCaseFunction): void { - if (process.env.COLLECT) { - console.log(label) - return - } - - try { - testCase() - console.log(greenText(`[PASSED] ${label}`)) - } catch (e) { - console.group(redText(`[FAILED] ${label}`)) - console.log(redText(String(e))) - console.groupEnd() - } -} - -const it = test - -export { it, test, expect, describe } diff --git a/src/testComponents/describe.ts b/src/testComponents/describe.ts new file mode 100644 index 0000000..ec149fa --- /dev/null +++ b/src/testComponents/describe.ts @@ -0,0 +1,43 @@ +import { promises as fs } from 'fs' + +import { greenText, redText } from '../utils' +import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from '../types' + +/* + * `describe` facilitates grouping tests together. + * + * ``` + * describe('My test group', () => { + * test('My first test', ...) + * + * test('My second test', ...) + * }) + * ``` + */ +function describe(label: TestCaseLabel, testGroup: TestCaseGroup) { + if (process.env.COLLECT) { + testGroup() + return + } + + console.group(greenText(label)) + testGroup() + console.groupEnd() +} + +Object.defineProperty(describe, 'each', { + value: function (values: Array) { + return (label: TestCaseLabel, testGroup: TestCaseGroup) => { + values.forEach((value: unknown, index: number) => { + describe(label.replace(/%s/g, String(value)), () => testGroup(value)) + }) + } + }, + enumerable: true, +}) + +type extendedDescribe = typeof describe & { [key: string]: (...args: Array) => extendedDescribe } + +const extDescribe = describe as extendedDescribe + +export default extDescribe diff --git a/src/expect.ts b/src/testComponents/expect.ts similarity index 99% rename from src/expect.ts rename to src/testComponents/expect.ts index 43ad143..e932ab1 100644 --- a/src/expect.ts +++ b/src/testComponents/expect.ts @@ -9,7 +9,7 @@ import { type RawComparisonMatcher, type RawMatchersMap, type MatcherName, -} from './types' +} from '../types' import matchers from './matchers' diff --git a/src/matchers.ts b/src/testComponents/matchers.ts similarity index 97% rename from src/matchers.ts rename to src/testComponents/matchers.ts index 6b9f300..9f060dd 100644 --- a/src/matchers.ts +++ b/src/testComponents/matchers.ts @@ -6,7 +6,7 @@ */ import assert from 'assert' -import { type MatcherReport } from './types' +import { type MatcherReport } from '../types' /* * Asserts whether value and other are strictly equal. diff --git a/src/testComponents/test.ts b/src/testComponents/test.ts new file mode 100644 index 0000000..b9259ec --- /dev/null +++ b/src/testComponents/test.ts @@ -0,0 +1,53 @@ +import { promises as fs } from 'fs' + +import { greenText, redText } from '../utils' +import { type TestCaseLabel, type TestCaseFunction, type TestCaseGroup } from '../types' + +/* + * `test` defines a single test case. + * + * ``` + * test('My test', () => { + * // Assert things. + * }) + * ``` + */ +function test(label: TestCaseLabel, testCase: TestCaseFunction): void { + if (process.env.COLLECT) { + console.log(label) + return + } + + try { + testCase() + console.log(greenText(`[PASSED] ${label}`)) + } catch (e) { + console.group(redText(`[FAILED] ${label}`)) + console.log(redText(String(e))) + console.groupEnd() + } +} + +Object.defineProperty(test, 'each', { + value: function (values: Array) { + return (label: TestCaseLabel, testCase: TestCaseFunction) => { + values.forEach((value: unknown, index: number) => { + test(label.replace(/%s/g, String(value)), () => testCase(value)) + }) + } + }, + enumerable: true, +}) + +type extendedTest = typeof test & { [key: string]: (...args: Array) => extendedTest } + +const extTest = test as extendedTest + +/* + * `it` is an alias of `test`. + */ +const it = extTest + +export { it } + +export default extTest diff --git a/src/types.ts b/src/types.ts index 6e200a3..d6e499f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,8 +2,8 @@ import { type Server } from 'net' export type TestCaseLabel = string export type TestFilePath = string -export type TestCaseFunction = () => void -export type TestCaseGroup = () => void +export type TestCaseFunction = (...args: Array) => void +export type TestCaseGroup = (...args: Array) => void export interface TestServer extends Server { failure?: boolean diff --git a/tests/expect.test.ts b/tests/expect.test.ts index a6745bb..b5f8650 100644 --- a/tests/expect.test.ts +++ b/tests/expect.test.ts @@ -1,55 +1,17 @@ import assert from 'assert' -import { describe, test, expect } from '../src/testCaseUtils' +import { describe, test, expect } from '../src' describe('Equality', () => { - test('Equality (number)', () => { - assert.doesNotThrow(() => expect(1).toEqual(1)) + test.each([1, 'expectations', true])('Equality (value=%s)', (value: unknown) => { + assert.doesNotThrow(() => expect(value).toEqual(value)) }) - test('Equality (string)', () => { - assert.doesNotThrow(() => expect('expectations').toEqual('expectations')) - }) - - test('Equality (boolean)', () => { - assert.doesNotThrow(() => expect(true).toEqual(true)) - }) - - test('Equality (failed - number)', () => { - assert.throws(() => expect(1).toEqual(2)) - }) - - test('Equality (failed - string)', () => { - assert.throws(() => expect('expectation').toEqual('something else')) - }) - - test('Equality (failed - boolean)', () => { - assert.throws(() => expect(true).toEqual(false)) - }) -}) - -describe('Identity', () => { - test('Identity comparison (number)', () => { - assert.doesNotThrow(() => expect(1).toBe(1)) - }) - - test('Identity comparison (boolean)', () => { - assert.doesNotThrow(() => expect(true).toBe(true)) - }) - - test('Identity comparison (string)', () => { - assert.doesNotThrow(() => expect('identity').toBe('identity')) - }) - - test('Identity comparison (failed - number)', () => { - assert.throws(() => expect(1).toEqual(2)) - }) - - test('Identity comparison (failed - boolean)', () => { - assert.throws(() => expect(false).toBe(true)) - }) - - test('Identity comparison (failed - string)', () => { - assert.throws(() => expect('yes').toBe('no')) + test.each([ + [1, 2], + ['expectation', 'something else'], + [true, false], + ])('Equality (failed - values=%s)', (...pair: Array) => { + assert.throws(() => expect(pair[0]).toEqual(pair[1])) }) test('Equality negation', () => { @@ -57,6 +19,24 @@ describe('Identity', () => { }) }) +describe('Identity', () => { + test.each([1, true, 'identity'])('Identity comparison (value=%s)', (value: unknown) => { + assert.doesNotThrow(() => expect(value).toBe(value)) + }) + + test.each([ + [1, 2], + [false, true], + ['yes', 'no'], + ])('Identity comparison (failed - value=%s)', (...pair: Array) => { + assert.throws(() => expect(pair[0]).toBe(pair[1])) + }) + + test('Identity negation', () => { + assert.doesNotThrow(() => expect('yes').not.toBe('no')) + }) +}) + describe('Exception expectation', () => { test('Expects error', () => { const err = new Error('err') diff --git a/tests/it.test.ts b/tests/it.test.ts new file mode 100644 index 0000000..a304da4 --- /dev/null +++ b/tests/it.test.ts @@ -0,0 +1,14 @@ +import assert from 'assert' + +import { it, test, expect, describe } from '../src' + +describe.each([it, test])('Runs tests', (fn: unknown) => { + const testFn = fn as typeof test + testFn('Runs a test', () => { + assert.doesNotThrow(() => expect(1).toEqual(1)) + }) + + testFn.each([1, 2, 3])('Supports parametrization (value=%s)', (value: unknown) => { + assert.doesNotThrow(() => expect(value).toEqual(value)) + }) +})