Skip to content

Commit deb17c6

Browse files
MrLeebokeeganwitt
andauthored
Closes #253: Add change matchers, .toChange(), .toChangeBy(), .toChangeTo() (#521)
* Add change matchers: .toChange(), .toChangeBy(), and .toChangeTo() * Add docs for change matchers * Fix incomplete test description * Applies notes from code review: improves typescript types and asserts support for non-numeric values * Migrate to TypeScript * Use unknown instead of any --------- Co-authored-by: Keegan Witt <[email protected]>
1 parent 93600e4 commit deb17c6

File tree

14 files changed

+468
-0
lines changed

14 files changed

+468
-0
lines changed

src/matchers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export { toBeSymbol } from './toBeSymbol';
3434
export { toBeTrue } from './toBeTrue';
3535
export { toBeValidDate } from './toBeValidDate';
3636
export { toBeWithin } from './toBeWithin';
37+
export { toChange } from './toChange';
38+
export { toChangeBy } from './toChangeBy';
39+
export { toChangeTo } from './toChangeTo';
3740
export { toContainAllEntries } from './toContainAllEntries';
3841
export { toContainAllKeys } from './toContainAllKeys';
3942
export { toContainAllValues } from './toContainAllValues';

src/matchers/toChange.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Use `.toChange` when checking if a value has changed.
3+
* @example
4+
* expect(() => value--).toChange(() => value);
5+
*/
6+
export function toChange(mutator: () => unknown | void, checker: () => number) {
7+
// @ts-expect-error OK to have implicit any for this.utils
8+
const { printReceived: print, matcherHint: hint } = this.utils;
9+
10+
const before = checker();
11+
mutator();
12+
const received = checker();
13+
14+
const passMessage = `
15+
${hint('.not.toChange', 'received', '')}\n\nExpected value to not change, received:\n ${print(received)}
16+
`.trim();
17+
const failMessage = `
18+
${hint('.toChange', 'received', '')}\n\nExpected value to change, received:\n ${print(received)}
19+
`.trim();
20+
21+
const pass = received !== before;
22+
return { pass, message: () => (pass ? passMessage : failMessage) };
23+
}

src/matchers/toChangeBy.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Use `.toChangeBy` when checking if a value changed by an amount.
3+
* @example
4+
* expect(() => value--).toChangeBy(() => value, -1);
5+
*/
6+
export function toChangeBy(mutator: () => unknown | void, checker: () => number, by: number = 1) {
7+
// @ts-expect-error OK to have implicit any for this.utils
8+
const { printReceived: print, matcherHint: hint } = this.utils;
9+
10+
const before = checker();
11+
mutator();
12+
const received = checker() - before;
13+
14+
const passMessage = `
15+
${hint('.not.toChangeBy', 'received', '')}\n\nExpected value to not change by ${by}, received:\n ${print(received)}
16+
`.trim();
17+
const failMessage = `
18+
${hint('.toChangeBy', 'received', '')}\n\nExpected value to change by ${by}, received:\n ${print(received)}
19+
`.trim();
20+
21+
const pass = received === by;
22+
return { pass, message: () => (pass ? passMessage : failMessage) };
23+
}

src/matchers/toChangeTo.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Use `.toChangeTo` when checking if a value changed to a specific value.
3+
* @example
4+
* expect(() => Model.deleteAll()).toChangeTo(() => Model.count(), 0);
5+
*/
6+
export function toChangeTo(mutator: () => unknown | void, checker: () => number, to: number) {
7+
// @ts-expect-error OK to have implicit any for this.utils
8+
const { printReceived: print, matcherHint: hint } = this.utils;
9+
10+
const before = checker();
11+
12+
if (before === to) {
13+
const noChangeMessage = `${hint(
14+
'.toChangeTo',
15+
'received',
16+
'',
17+
)}\n\nCannot expect a value to change to its original state, received: ${print(before)}`;
18+
return { pass: false, message: () => noChangeMessage };
19+
}
20+
21+
mutator();
22+
const received = checker();
23+
24+
const passMessage = `
25+
${hint('.not.toChangeTo', 'received', '')}\n\nExpected value to not change to ${to}, received:\n ${print(received)}
26+
`.trim();
27+
const failMessage = `
28+
${hint('.toChangeto', 'received', '')}\n\nExpected value to change to ${to}, received:\n ${print(received)}
29+
`.trim();
30+
31+
const pass = received !== before && received === to;
32+
return { pass, message: () => (pass ? passMessage : failMessage) };
33+
}

test/fixtures/counter.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export class Counter {
2+
private value: number;
3+
4+
constructor() {
5+
this.increment = this.increment.bind(this);
6+
this.count = this.count.bind(this);
7+
this.reset = this.reset.bind(this);
8+
this.value = 0;
9+
}
10+
11+
increment() {
12+
this.value++;
13+
}
14+
15+
count() {
16+
return this.value;
17+
}
18+
19+
reset() {
20+
this.value = 0;
21+
}
22+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`.not.toChange fails when a value expected not to change defies all expectations and changes 1`] = `
4+
"<dim>expect(</intensity><red>received</color><dim>).not.toChange()</intensity>
5+
6+
Expected value to not change, received:
7+
<red>1</color>"
8+
`;
9+
10+
exports[`.not.toChange fails when mutating a counter 1`] = `
11+
"<dim>expect(</intensity><red>received</color><dim>).not.toChange()</intensity>
12+
13+
Expected value to not change, received:
14+
<red>1</color>"
15+
`;
16+
17+
exports[`.toChange fails when a given value does not increment 1`] = `
18+
"<dim>expect(</intensity><red>received</color><dim>).toChange()</intensity>
19+
20+
Expected value to change, received:
21+
<red>0</color>"
22+
`;
23+
24+
exports[`.toChange fails when not mutating a counter 1`] = `
25+
"<dim>expect(</intensity><red>received</color><dim>).toChange()</intensity>
26+
27+
Expected value to change, received:
28+
<red>0</color>"
29+
`;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`.not.toChangeBy fails when a value expected not to change defies all expectations and changes 1`] = `
4+
"<dim>expect(</intensity><red>received</color><dim>).not.toChangeBy()</intensity>
5+
6+
Expected value to not change by 1, received:
7+
<red>1</color>"
8+
`;
9+
10+
exports[`.not.toChangeBy fails when mutating a counter 1`] = `
11+
"<dim>expect(</intensity><red>received</color><dim>).not.toChangeBy()</intensity>
12+
13+
Expected value to not change by 1, received:
14+
<red>1</color>"
15+
`;
16+
17+
exports[`.toChangeBy fails when a given value does not increment 1`] = `
18+
"<dim>expect(</intensity><red>received</color><dim>).toChangeBy()</intensity>
19+
20+
Expected value to change by 1, received:
21+
<red>0</color>"
22+
`;
23+
24+
exports[`.toChangeBy fails when not mutating a counter 1`] = `
25+
"<dim>expect(</intensity><red>received</color><dim>).toChangeBy()</intensity>
26+
27+
Expected value to change by 1, received:
28+
<red>0</color>"
29+
`;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`.not.toChange fails when a value expected not to change defies all expectations and changes 1`] = `
4+
"<dim>expect(</intensity><red>received</color><dim>).not.toChangeTo()</intensity>
5+
6+
Expected value to not change to 1, received:
7+
<red>1</color>"
8+
`;
9+
10+
exports[`.not.toChange fails when mutating a counter 1`] = `
11+
"<dim>expect(</intensity><red>received</color><dim>).not.toChangeTo()</intensity>
12+
13+
Expected value to not change to 1, received:
14+
<red>1</color>"
15+
`;
16+
17+
exports[`.toChangeTo fails when a given value does not increment 1`] = `
18+
"<dim>expect(</intensity><red>received</color><dim>).toChangeto()</intensity>
19+
20+
Expected value to change to 1, received:
21+
<red>0</color>"
22+
`;
23+
24+
exports[`.toChangeTo fails when not mutating a counter 1`] = `
25+
"<dim>expect(</intensity><red>received</color><dim>).toChangeto()</intensity>
26+
27+
Expected value to change to 1, received:
28+
<red>0</color>"
29+
`;
30+
31+
exports[`.toChangeTo fails when resetting a counter back because a change cannot be the same as the initial value 1`] = `
32+
"<dim>expect(</intensity><red>received</color><dim>).toChangeTo()</intensity>
33+
34+
Cannot expect a value to change to its original state, received: <red>0</color>"
35+
`;

test/matchers/toChange.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as matcher from 'src/matchers/toChange';
2+
import { Counter } from '../fixtures/counter';
3+
4+
expect.extend(matcher);
5+
6+
describe('.toChange', () => {
7+
test('passes when given a value that the mutator increments', () => {
8+
let value = 0;
9+
expect(() => value++).toChange(() => value);
10+
});
11+
12+
test('passes when mutating a counter', () => {
13+
const counter = new Counter();
14+
expect(counter.increment).toChange(counter.count);
15+
});
16+
17+
test('fails when a given value does not increment', () => {
18+
const value = 0;
19+
expect(() => expect(() => value).toChange(() => value)).toThrowErrorMatchingSnapshot();
20+
});
21+
22+
test('fails when not mutating a counter', () => {
23+
const counter = new Counter();
24+
expect(() => expect(counter.count).toChange(counter.count)).toThrowErrorMatchingSnapshot();
25+
});
26+
});
27+
28+
describe('.not.toChange', () => {
29+
test('passes when given a mutator that does not increment the value', () => {
30+
const value = 0;
31+
expect(() => value).not.toChange(() => value);
32+
});
33+
34+
test('passes when not mutating a counter', () => {
35+
const counter = new Counter();
36+
expect(counter.count).not.toChange(counter.count);
37+
});
38+
39+
test('fails when mutating a counter', () => {
40+
const counter = new Counter();
41+
expect(() => expect(counter.increment).not.toChange(counter.count)).toThrowErrorMatchingSnapshot();
42+
});
43+
44+
test('fails when a value expected not to change defies all expectations and changes', () => {
45+
let value = 0;
46+
expect(() => expect(() => value++).not.toChange(() => value)).toThrowErrorMatchingSnapshot();
47+
});
48+
});

test/matchers/toChangeBy.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as matcher from 'src/matchers/toChangeBy';
2+
import { Counter } from '../fixtures/counter';
3+
4+
expect.extend(matcher);
5+
6+
describe('.toChangeBy', () => {
7+
test('passes when given a value that the mutator increments', () => {
8+
let value = 0;
9+
expect(() => value--).toChangeBy(() => value, -1);
10+
});
11+
12+
test('passes when mutating a counter', () => {
13+
const counter = new Counter();
14+
expect(counter.increment).toChangeBy(counter.count);
15+
});
16+
17+
test('passes when resetting a counter', () => {
18+
const counter = new Counter();
19+
counter.increment();
20+
expect(() => {
21+
counter.increment();
22+
counter.increment();
23+
}).toChangeBy(() => counter.count(), 2);
24+
});
25+
26+
test('fails when a given value does not increment', () => {
27+
const value = 0;
28+
expect(() => expect(() => value).toChangeBy(() => value)).toThrowErrorMatchingSnapshot();
29+
});
30+
31+
test('fails when not mutating a counter', () => {
32+
const counter = new Counter();
33+
expect(() => expect(counter.count).toChangeBy(counter.count)).toThrowErrorMatchingSnapshot();
34+
});
35+
36+
test('passes when resetting a counter back', () => {
37+
const counter = new Counter();
38+
counter.increment();
39+
counter.increment();
40+
counter.increment();
41+
expect(() => counter.reset()).toChangeBy(() => counter.count(), -3);
42+
});
43+
});
44+
45+
describe('.not.toChangeBy', () => {
46+
test('passes when given a mutator that does not increment the value', () => {
47+
const value = 0;
48+
expect(() => value).not.toChangeBy(() => value, 1);
49+
});
50+
51+
test('passes when not mutating a counter', () => {
52+
const counter = new Counter();
53+
expect(counter.count).not.toChangeBy(counter.count, 1);
54+
});
55+
56+
test('fails when resetting a counter', () => {
57+
const counter = new Counter();
58+
expect(() => {
59+
counter.increment();
60+
counter.increment();
61+
counter.increment();
62+
counter.reset();
63+
}).not.toChangeBy(() => counter.count(), -3);
64+
});
65+
66+
test('fails when mutating a counter', () => {
67+
const counter = new Counter();
68+
expect(() => expect(counter.increment).not.toChangeBy(counter.count, 1)).toThrowErrorMatchingSnapshot();
69+
});
70+
71+
test('fails when a value expected not to change defies all expectations and changes', () => {
72+
let value = 0;
73+
expect(() => expect(() => value++).not.toChangeBy(() => value, 1)).toThrowErrorMatchingSnapshot();
74+
});
75+
});

0 commit comments

Comments
 (0)