Skip to content

Commit fadc6c8

Browse files
committed
Add test.macro(), change how macros are typed
Fixes #2189. `test.macro()` returns an object that can be used with `test()` and hooks. The `t.context` type is inherited from `test`. Like with AVA 3, regular functions that also have a `title` property that is a string-returning function are supported. However this is no longer in the type definition. Instead the recommended approach is to use `test.macro()` to declare macros. At a TypeScript level these are easier to discriminate from regular implementations.
1 parent a8cbfb7 commit fadc6c8

File tree

12 files changed

+263
-135
lines changed

12 files changed

+263
-135
lines changed

.xo-config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@
7171
"import/no-extraneous-dependencies": "off",
7272
"import/no-unresolved": "off"
7373
}
74+
},
75+
{
76+
"files": "test/macros/fixtures/macros.js",
77+
"rules": {
78+
"ava/no-identical-title": "off"
79+
}
7480
}
7581
]
7682
}

docs/01-writing-tests.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ console.log('Test file currently being run:', test.meta.file);
294294

295295
Additional arguments passed to the test declaration will be passed to the test implementation. This is useful for creating reusable test macros.
296296

297+
You can use plain functions:
298+
297299
```js
298300
function macro(t, input, expected) {
299301
t.is(eval(input), expected);
@@ -303,7 +305,7 @@ test('2 + 2 = 4', macro, '2 + 2', 4);
303305
test('2 * 3 = 6', macro, '2 * 3', 6);
304306
```
305307

306-
You can build the test title programmatically by attaching a `title` function to the macro:
308+
With AVA 3 you can build the test title programmatically by attaching a `title` function to the macro:
307309

308310
```js
309311
function macro(t, input, expected) {
@@ -319,4 +321,35 @@ test('providedTitle', macro, '3 * 3', 9);
319321

320322
The `providedTitle` argument defaults to `undefined` if the user does not supply a string title. This means you can use a parameter assignment to set the default value. The example above uses the empty string as the default.
321323

324+
However with AVA 4 the preferred approach is to use the `test.macro()` helper:
325+
326+
```js
327+
import test from 'ava';
328+
329+
const macro = test.macro((t, input, expected) => {
330+
t.is(eval(input), expected);
331+
});
332+
333+
test('title', macro, '3 * 3', 9);
334+
```
335+
336+
Or with a title function:
337+
338+
```js
339+
import test from 'ava';
340+
341+
const macro = test.macro({
342+
exec(t, input, expected) {
343+
t.is(eval(input), expected);
344+
},
345+
title(providedTitle = '', input, expected) {
346+
return `${providedTitle} ${input} = ${expected}`.trim();
347+
}
348+
});
349+
350+
test(macro, '2 + 2', 4);
351+
test(macro, '2 * 3', 6);
352+
test('providedTitle', macro, '3 * 3', 9);
353+
```
354+
322355
We encourage you to use macros instead of building your own test generators ([here is an example](https://github.com/avajs/ava-codemods/blob/47073b5b58aa6f3fb24f98757be5d3f56218d160/test/ok-to-truthy.js#L7-L9) of code that should be replaced with a macro). Macros are designed to perform static analysis of your code, which can lead to better performance, IDE integration, and linter rules.

docs/recipes/typescript.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ const hasLength = (t: ExecutionContext, input: string, expected: number) => {
121121
test('bar has length 3', hasLength, 'bar', 3);
122122
```
123123

124-
In order to be able to assign the `title` property to a macro you need to type the function:
124+
### AVA 3
125+
126+
With AVA 3, in order to be able to assign the `title` property to a macro you need to type the function:
125127

126128
```ts
127129
import test, {Macro} from 'ava';
@@ -149,6 +151,39 @@ const macro: CbMacro<[]> = t => {
149151
test.cb(macro);
150152
```
151153

154+
### AVA 4
155+
156+
With AVA 4 you can use the `test.macro()` helper to create macros:
157+
158+
```ts
159+
import test from 'ava';
160+
161+
const macro = test.macro((t, input: string, expected: number) => {
162+
t.is(eval(input), expected);
163+
});
164+
165+
test('title', macro, '3 * 3', 9);
166+
```
167+
168+
Or with a title function:
169+
170+
```ts
171+
import test from 'ava';
172+
173+
const macro = test.macro({
174+
exec(t, input: string, expected: number) {
175+
t.is(eval(input), expected);
176+
},
177+
title(providedTitle = '', input, expected) {
178+
return `${providedTitle} ${input} = ${expected}`.trim();
179+
}
180+
});
181+
182+
test(macro, '2 + 2', 4);
183+
test(macro, '2 * 3', 6);
184+
test('providedTitle', macro, '3 * 3', 9);
185+
```
186+
152187
## Typing [`t.context`](../01-writing-tests.md#test-context)
153188

154189
By default, the type of `t.context` will be the empty object (`{}`). AVA exposes an interface `TestInterface<Context>` which you can use to apply your own type to `t.context`. This can help you catch errors at compile-time:

index.d.ts

Lines changed: 68 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -401,19 +401,37 @@ export interface TeardownFn {
401401
(fn: () => void): void;
402402
}
403403

404+
export type ImplementationFn<Args extends any[], Context = unknown> =
405+
((t: ExecutionContext<Context>, ...args: Args) => PromiseLike<void>) |
406+
((t: ExecutionContext<Context>, ...args: Args) => Subscribable) |
407+
((t: ExecutionContext<Context>, ...args: Args) => void);
408+
409+
export type TitleFn<Args extends any[]> = (providedTitle: string | undefined, ...args: Args) => string;
410+
411+
/** A reusable test or hook implementation. */
412+
export type Macro<Args extends any[], Context = unknown> = {
413+
/** The function that is executed when the macro is used. */
414+
readonly exec: ImplementationFn<Args, Context>;
415+
416+
/** Generates a test title when this macro is used. */
417+
readonly title?: TitleFn<Args>;
418+
};
419+
420+
/** A test or hook implementation. */
421+
export type Implementation<Args extends any[], Context = unknown> = ImplementationFn<Args, Context> | Macro<Args, Context>;
422+
404423
export interface TryFn<Context = unknown> {
405424
/**
406425
* Attempt to run some assertions. The result must be explicitly committed or discarded or else
407-
* the test will fail. A macro may be provided. The title may help distinguish attempts from
408-
* one another.
426+
* the test will fail. The title may help distinguish attempts from one another.
409427
*/
410-
<Args extends any[]>(title: string, fn: EitherMacro<Args, Context>, ...args: Args): Promise<TryResult>;
428+
<Args extends any[]>(title: string, fn: Implementation<Args, Context>, ...args: Args): Promise<TryResult>;
411429

412430
/**
413431
* Attempt to run some assertions. The result must be explicitly committed or discarded or else
414-
* the test will fail. A macro may be provided.
432+
* the test will fail.
415433
*/
416-
<Args extends any[]>(fn: EitherMacro<Args, Context>, ...args: Args): Promise<TryResult>;
434+
<Args extends any[]>(fn: Implementation<Args, Context>, ...args: Args): Promise<TryResult>;
417435
}
418436

419437
export interface AssertionError extends Error {}
@@ -451,34 +469,15 @@ export interface TryResult {
451469
discard(options?: CommitDiscardOptions): void;
452470
}
453471

454-
// FIXME(novemberborn) Refactor implementations to be different types returning a promise,, subscribable, or void, not a
455-
// single type returning a union. A union with void as a return type doesn't make sense.
456-
export type ImplementationResult = PromiseLike<void> | Subscribable | boolean | void;
457-
export type Implementation<Context = unknown> = (t: ExecutionContext<Context>) => ImplementationResult;
458-
459-
/** A reusable test or hook implementation. */
460-
export type UntitledMacro<Args extends any[], Context = unknown> = (t: ExecutionContext<Context>, ...args: Args) => ImplementationResult;
472+
export interface TestInterface<Context = unknown> {
473+
/** Declare a concurrent test. Additional arguments are passed along. */
474+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
461475

462-
/** A reusable test or hook implementation. */
463-
export type Macro<Args extends any[], Context = unknown> = UntitledMacro<Args, Context> & {
464476
/**
465-
* Implement this function to generate a test (or hook) title whenever this macro is used. `providedTitle` contains
466-
* the title provided when the test or hook was declared. Also receives the remaining test arguments.
477+
* Declare a concurrent test that uses a macro. The macro is responsible for generating a unique test title.
478+
* Additional arguments are passed along.
467479
*/
468-
title?: (providedTitle: string | undefined, ...args: Args) => string;
469-
};
470-
471-
export type EitherMacro<Args extends any[], Context> = Macro<Args, Context> | UntitledMacro<Args, Context>;
472-
473-
export interface TestInterface<Context = unknown> {
474-
/** Declare a concurrent test. */
475-
(title: string, implementation: Implementation<Context>): void;
476-
477-
/** Declare a concurrent test that uses a macro. Additional arguments are passed to the macro. */
478-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
479-
480-
/** Declare a concurrent test that uses a macro. The macro is responsible for generating a unique test title. */
481-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
480+
<Args extends any[]>(macro: Macro<Args, Context>, ...args: Args): void;
482481

483482
/** Declare a hook that is run once, after all tests have passed. */
484483
after: AfterInterface<Context>;
@@ -501,21 +500,16 @@ export interface TestInterface<Context = unknown> {
501500
only: OnlyInterface<Context>;
502501
skip: SkipInterface<Context>;
503502
todo: TodoDeclaration;
503+
macro: MacroDeclaration<Context>;
504504
meta: MetaInterface;
505505
}
506506

507507
export interface AfterInterface<Context = unknown> {
508-
/** Declare a hook that is run once, after all tests have passed. */
509-
(implementation: Implementation<Context>): void;
508+
/** Declare a hook that is run once, after all tests have passed. Additional arguments are passed along. */
509+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
510510

511-
/** Declare a hook that is run once, after all tests have passed. */
512-
(title: string, implementation: Implementation<Context>): void;
513-
514-
/** Declare a hook that is run once, after all tests have passed. Additional arguments are passed to the macro. */
515-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
516-
517-
/** Declare a hook that is run once, after all tests have passed. */
518-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
511+
/** Declare a hook that is run once, after all tests have passed. Additional arguments are passed along. */
512+
<Args extends any[]>(implementation: Implementation<Args, Context>, ...args: Args): void;
519513

520514
/** Declare a hook that is run once, after all tests are done. */
521515
always: AlwaysInterface<Context>;
@@ -524,99 +518,66 @@ export interface AfterInterface<Context = unknown> {
524518
}
525519

526520
export interface AlwaysInterface<Context = unknown> {
527-
/** Declare a hook that is run once, after all tests are done. */
528-
(implementation: Implementation<Context>): void;
529-
530-
/** Declare a hook that is run once, after all tests are done. */
531-
(title: string, implementation: Implementation<Context>): void;
521+
/** Declare a hook that is run once, after all tests are done. Additional arguments are passed along. */
522+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
532523

533-
/** Declare a hook that is run once, after all tests are done. Additional arguments are passed to the macro. */
534-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
535-
536-
/** Declare a hook that is run once, after all tests are done. */
537-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
524+
/** Declare a hook that is run once, after all tests are done. Additional arguments are passed along. */
525+
<Args extends any[]>(implementation: Implementation<Args, Context>, ...args: Args): void;
538526

539527
skip: HookSkipInterface<Context>;
540528
}
541529

542530
export interface BeforeInterface<Context = unknown> {
543-
/** Declare a hook that is run once, before all tests. */
544-
(implementation: Implementation<Context>): void;
531+
/** Declare a hook that is run once, before all tests. Additional arguments are passed along. */
532+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
545533

546-
/** Declare a hook that is run once, before all tests. */
547-
(title: string, implementation: Implementation<Context>): void;
548-
549-
/** Declare a hook that is run once, before all tests. Additional arguments are passed to the macro. */
550-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
551-
552-
/** Declare a hook that is run once, before all tests. */
553-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
534+
/** Declare a hook that is run once, before all tests. Additional arguments are passed along. */
535+
<Args extends any[]>(implementation: Implementation<Args, Context>, ...args: Args): void;
554536

555537
skip: HookSkipInterface<Context>;
556538
}
557539

558540
export interface FailingInterface<Context = unknown> {
559-
/** Declare a concurrent test. The test is expected to fail. */
560-
(title: string, implementation: Implementation<Context>): void;
541+
/** Declare a concurrent test. Additional arguments are passed along. The test is expected to fail. */
542+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
561543

562544
/**
563-
* Declare a concurrent test that uses a macro. Additional arguments are passed to the macro.
564-
* The test is expected to fail.
545+
* Declare a concurrent test that uses a macro. Additional arguments are passed along.
546+
* The macro is responsible for generating a unique test title. The test is expected to fail.
565547
*/
566-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
567-
568-
/**
569-
* Declare a concurrent test that uses a macro. The macro is responsible for generating a unique test title.
570-
* The test is expected to fail.
571-
*/
572-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
548+
<Args extends any[]>(macro: Macro<Args, Context>, ...args: Args): void;
573549

574550
only: OnlyInterface<Context>;
575551
skip: SkipInterface<Context>;
576552
}
577553

578554
export interface HookSkipInterface<Context = unknown> {
579555
/** Skip this hook. */
580-
(implementation: Implementation<Context>): void;
581-
582-
/** Skip this hook. */
583-
(title: string, implementation: Implementation<Context>): void;
556+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
584557

585558
/** Skip this hook. */
586-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
587-
588-
/** Skip this hook. */
589-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
559+
<Args extends any[]>(implementation: Implementation<Args, Context>, ...args: Args): void;
590560
}
591561

592562
export interface OnlyInterface<Context = unknown> {
593-
/** Declare a test. Only this test and others declared with `.only()` are run. */
594-
(title: string, implementation: Implementation<Context>): void;
595-
596-
/**
597-
* Declare a test that uses a macro. Additional arguments are passed to the macro.
598-
* Only this test and others declared with `.only()` are run.
599-
*/
600-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
563+
/** Declare a test. Additional arguments are passed along. Only this test and others declared with `.only()` are run. */
564+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
601565

602566
/**
603567
* Declare a test that uses a macro. The macro is responsible for generating a unique test title.
604568
* Only this test and others declared with `.only()` are run.
605569
*/
606-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
570+
<Args extends any[]>(macro: Macro<Args, Context>, ...args: Args): void;
607571
}
608572

609573
export interface SerialInterface<Context = unknown> {
610-
/** Declare a serial test. */
611-
(title: string, implementation: Implementation<Context>): void;
612-
613-
/** Declare a serial test that uses a macro. Additional arguments are passed to the macro. */
614-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
574+
/** Declare a serial test. Additional arguments are passed along. */
575+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
615576

616577
/**
617578
* Declare a serial test that uses a macro. The macro is responsible for generating a unique test title.
618579
*/
619-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
580+
<Args extends any[]>(macro: Macro<Args, Context>, ...args: Args): void;
620581

621582
/** Declare a serial hook that is run once, after all tests have passed. */
622583
after: AfterInterface<Context>;
@@ -640,20 +601,28 @@ export interface SerialInterface<Context = unknown> {
640601

641602
export interface SkipInterface<Context = unknown> {
642603
/** Skip this test. */
643-
(title: string, implementation: Implementation<Context>): void;
604+
<Args extends any[]>(title: string, implementation: Implementation<Args, Context>, ...args: Args): void;
644605

645606
/** Skip this test. */
646-
<T extends any[]>(title: string, macro: EitherMacro<T, Context>, ...rest: T): void;
647-
648-
/** Skip this test. */
649-
<T extends any[]>(macro: EitherMacro<T, Context>, ...rest: T): void;
607+
<Args extends any[]>(macro: Macro<Args, Context>, ...args: Args): void;
650608
}
651609

652610
export interface TodoDeclaration {
653611
/** Declare a test that should be implemented later. */
654612
(title: string): void;
655613
}
656614

615+
export type MacroDeclarationOptions<Args extends any[], Context = unknown> = {
616+
exec: ImplementationFn<Args, Context>;
617+
title: TitleFn<Args>;
618+
};
619+
620+
export interface MacroDeclaration<Context = unknown> {
621+
/** Declare a reusable test implementation. */
622+
<Args extends any[]>(exec: ImplementationFn<Args, Context>): Macro<Args, Context>;
623+
<Args extends any[]>(declaration: MacroDeclarationOptions<Args, Context>): Macro<Args, Context>;
624+
}
625+
657626
export interface MetaInterface {
658627
/** Path to the test file being executed. */
659628
file: string;

lib/create-chain.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ export default function createChain(fn, defaults, meta) {
9191
root.todo = startChain('test.todo', fn, {...defaults, type: 'test', todo: true});
9292
root.serial.todo = startChain('test.serial.todo', fn, {...defaults, serial: true, type: 'test', todo: true});
9393

94+
root.macro = options => {
95+
if (typeof options === 'function') {
96+
return Object.freeze({exec: options});
97+
}
98+
99+
return Object.freeze({exec: options.exec, title: options.title});
100+
};
101+
94102
root.meta = meta;
95103

96104
return root;

0 commit comments

Comments
 (0)