Repository: jest-community/jest-extended

Issue:
Expect a something to be change by amount
**Feature Request**

Description:

In other testing tool, there was a matcher that expects some value to be changed by specified amount or from a value to a value. Usually a counter.

Here is an example: https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/change-matcher#expect-change

I don't see a quick way to describe such behavior

Possible solution:

``` javascript
// Required
expect(() => {new Post().save()}.toChange(() => Post.count(), {by: 1});
// Shortcut of double toEqual call (not super useful IMO)
expect(() => {new Post().save()}.toChange(() => Post.count(), {from: 0, to: 1});
```

Expecting all of that to work when the predicate returns a `Promise<number>` and when it returns a plain `number`.


Discussion:
@rgibson518: This might be enough to get you started by extending `Expect` or for making a PR to this library.  We don't use this library, but I happened upon it while searching "jest rspec tochange matcher".   Forgive the typescript, perhaps its useful for anyone else who has stumbled here.

```typescript
export {};

declare global {
  namespace jest {
    interface Matchers<R> {
      toChange(checker: () => number, options: ChangeOptions): R;
    }
  }
}
type ChangeOptions = { by: number };

expect.extend({
  toChange(mutator: () => any, checker: () => number, options: ChangeOptions) {
    let before: number = checker();

    mutator();

    const pass = checker() - before === options.by;

    if (pass) {
      return {
        pass: true,
        message: () => `expected ${before} not to change by ${options.by}`,
      };
    } else {
      return {
        pass: false,
        message: () => `expected ${before} to change by ${options.by}`,
      };
    }
  },
});

describe('testing extends', () => {
  it('should pass', () => {
    let value = 1;
    expect(() => value++).toChange(() => value, { by: 1 });
  });
});
```

Obviously super simple, if you need/want async behavior or a `from => to` option syntax you'd have to add that.  But this is a good base I think for that kind of iteration. Good luck!@urkle: Here is a more advanced version that supports any change, not change or to changes.

```javascript
/**
 * Custom matcher to allow an "expect change" type matcher
 *
 * Usage:
 *   To check for change by an amount
 *
 *   expect(() => value++).toChange(() => value, {by: 1});
 *
 *   To check for change to a specific value
 *
 *   expect(() => value++).toChange(() => value, {to: 1});
 *
 *   To check for any change
 *
 *   expect(() => value++).toChange(() => value);
 *
 *   to check for no change
 *
 *   expect(() => value).not.toChange(() => value);
 */
expect.extend({
  toChange(mutator, checker, options = {}) {
    let before = checker();

    mutator();

    let mode = null;
    if ('by' in options) {
      mode = 'by';
    }
    if ('to' in options) {
      if (mode) {
        throw new Error('only to or by may be specified, not both');
      }
      mode = 'to';
    }
    if (!mode) {
      mode = 'change';
    }

    switch (mode) {
      case 'by': {
        if (this.isNot) { throw new Error('.not.toChange(by:) is not supported'); }

        const pass = checker() - before === options.by;

        if (pass) {
          return {
            pass: true,
            message: () => `expected ${before} not to change by ${options.by}`,
          };
        } else {
          return {
            pass: false,
            message: () => `expected ${before} to change by ${options.by}`,
          };
        }
      }
      case 'to': {
        if (this.isNot) { throw new Error('.not.toChange(to:) is not supported'); }

        const final = checker();
        const pass = final !== before && final === options.to;

        if (pass) {
          return {
            pass: true,
            message: () => `expected ${before} not to change to ${options.to}`,
          };
        } else {
          return {
            pass: false,
            message: () => `expected ${before} to change to ${options.to}`,
          };
        }
      }
      case 'change': {
        const final = checker();
        const pass = final !== before;

        if (pass) {
          return {
            pass: true,
            message: () => `expected ${before} not to change`,
          };
        } else {
          return {
            pass: false,
            message: () => `expected ${before} to change`,
          };
        }
      }
    }
  },
});
```@keeganwitt: Just to state the obvious, using currently available matchers, one could do

```javascript
test('my test', () => {
  expect(Post.count().toEqual(0);
  Post.save();
  expect(Post.count().toEqual(1);
});
```@cohesivejones: > Just to state the obvious, using currently available matchers, one could do
> 
> test('my test', () => {
>   expect(Post.count().toEqual(0);
>   Post.save();
>   expect(Post.count().toEqual(1);
> });

This would assume that the test cares about the count of Posts before the execution, not the fact that the value changed by 1@keeganwitt: > This would assume that the test cares about the count of Posts before the execution, not the fact that the value changed by 1

You don't have to assert the starting value. Wouldn't this be testing the same thing?

```typescript
test('count increases by 1 after save', () => {
  const initialCount = Post.count();
  Post.save();
  expect(Post.count()).toBe(initialCount + 1);
});
```

Edit: It is, that's what was discussed in the PR.

Solution patch:
diff --git a/src/matchers/index.ts b/src/matchers/index.ts
index ee11a66..ac420f6 100644
--- a/src/matchers/index.ts
+++ b/src/matchers/index.ts
@@ -34,6 +34,9 @@ export { toBeSymbol } from './toBeSymbol';
 export { toBeTrue } from './toBeTrue';
 export { toBeValidDate } from './toBeValidDate';
 export { toBeWithin } from './toBeWithin';
+export { toChange } from './toChange';
+export { toChangeBy } from './toChangeBy';
+export { toChangeTo } from './toChangeTo';
 export { toContainAllEntries } from './toContainAllEntries';
 export { toContainAllKeys } from './toContainAllKeys';
 export { toContainAllValues } from './toContainAllValues';
diff --git a/src/matchers/toBeAfter.ts b/src/matchers/toBeAfter.ts
index 4adc9f7..c0e68ae 100644
--- a/src/matchers/toBeAfter.ts
+++ b/src/matchers/toBeAfter.ts
@@ -1,5 +1,5 @@
 export function toBeAfter(actual: unknown, after: Date) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = actual instanceof Date && actual > after;
diff --git a/src/matchers/toBeAfterOrEqualTo.ts b/src/matchers/toBeAfterOrEqualTo.ts
index dd99213..d197af7 100644
--- a/src/matchers/toBeAfterOrEqualTo.ts
+++ b/src/matchers/toBeAfterOrEqualTo.ts
@@ -1,5 +1,5 @@
 export function toBeAfterOrEqualTo(actual: unknown, expected: Date) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = actual instanceof Date && actual >= expected;
diff --git a/src/matchers/toBeArray.ts b/src/matchers/toBeArray.ts
index 3a90fed..089cd83 100644
--- a/src/matchers/toBeArray.ts
+++ b/src/matchers/toBeArray.ts
@@ -1,5 +1,5 @@
 export function toBeArray(expected: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, printReceived } = this.utils;
 
   const pass = Array.isArray(expected);
diff --git a/src/matchers/toBeArrayOfSize.ts b/src/matchers/toBeArrayOfSize.ts
index 520d25e..df972f5 100644
--- a/src/matchers/toBeArrayOfSize.ts
+++ b/src/matchers/toBeArrayOfSize.ts
@@ -1,7 +1,7 @@
 import { determinePropertyMessage } from 'src/utils';
 
 export function toBeArrayOfSize(actual: unknown, expected: number) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printExpected, printReceived, matcherHint } = this.utils;
 
   const pass = Array.isArray(actual) && actual.length === expected;
diff --git a/src/matchers/toBeBefore.ts b/src/matchers/toBeBefore.ts
index 0edf135..986884e 100644
--- a/src/matchers/toBeBefore.ts
+++ b/src/matchers/toBeBefore.ts
@@ -1,5 +1,5 @@
 export function toBeBefore(actual: unknown, expected: Date) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, printReceived } = this.utils;
 
   const pass = actual instanceof Date && actual < expected;
diff --git a/src/matchers/toBeBeforeOrEqualTo.ts b/src/matchers/toBeBeforeOrEqualTo.ts
index 42f47d8..04314be 100644
--- a/src/matchers/toBeBeforeOrEqualTo.ts
+++ b/src/matchers/toBeBeforeOrEqualTo.ts
@@ -1,5 +1,5 @@
 export function toBeBeforeOrEqualTo(actual: unknown, expected: Date) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, printReceived } = this.utils;
 
   const pass = actual instanceof Date && actual <= expected;
diff --git a/src/matchers/toBeBetween.ts b/src/matchers/toBeBetween.ts
index 316b837..b98218d 100644
--- a/src/matchers/toBeBetween.ts
+++ b/src/matchers/toBeBetween.ts
@@ -1,5 +1,5 @@
 export function toBeBetween(actual: unknown, startDate: Date, endDate: Date) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, printReceived } = this.utils;
 
   const pass = actual instanceof Date && actual >= startDate && actual <= endDate;
diff --git a/src/matchers/toBeBoolean.ts b/src/matchers/toBeBoolean.ts
index 01b64a5..c1f013a 100644
--- a/src/matchers/toBeBoolean.ts
+++ b/src/matchers/toBeBoolean.ts
@@ -1,5 +1,5 @@
 export function toBeBoolean(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, printReceived } = this.utils;
 
   const pass = typeof actual === 'boolean' || actual instanceof Boolean;
diff --git a/src/matchers/toBeDate.ts b/src/matchers/toBeDate.ts
index ecd722e..a8d7d6f 100644
--- a/src/matchers/toBeDate.ts
+++ b/src/matchers/toBeDate.ts
@@ -1,5 +1,5 @@
 export function toBeDate(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, printReceived } = this.utils;
 
   const pass = actual instanceof Date && !isNaN(actual.getTime());
diff --git a/src/matchers/toBeDateString.ts b/src/matchers/toBeDateString.ts
index b78a3d7..ee65da2 100644
--- a/src/matchers/toBeDateString.ts
+++ b/src/matchers/toBeDateString.ts
@@ -1,5 +1,5 @@
 export function toBeDateString(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, printReceived } = this.utils;
 
   const pass = typeof actual === 'string' && !isNaN(Date.parse(actual));
diff --git a/src/matchers/toBeEmpty.ts b/src/matchers/toBeEmpty.ts
index 0fbe913..76c6de5 100644
--- a/src/matchers/toBeEmpty.ts
+++ b/src/matchers/toBeEmpty.ts
@@ -1,8 +1,8 @@
 export function toBeEmpty(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.equals
   const pass = this.equals({}, actual) || isEmptyIterable(actual);
 
   return {
diff --git a/src/matchers/toBeEmptyObject.ts b/src/matchers/toBeEmptyObject.ts
index d3032f2..cfadc33 100644
--- a/src/matchers/toBeEmptyObject.ts
+++ b/src/matchers/toBeEmptyObject.ts
@@ -1,5 +1,5 @@
 export function toBeEmptyObject(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass =
diff --git a/src/matchers/toBeEven.ts b/src/matchers/toBeEven.ts
index b618136..785bc16 100644
--- a/src/matchers/toBeEven.ts
+++ b/src/matchers/toBeEven.ts
@@ -1,5 +1,5 @@
 export function toBeEven(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = isNumber(actual) && isEven(actual);
diff --git a/src/matchers/toBeExtensible.ts b/src/matchers/toBeExtensible.ts
index 4c270c5..1d23146 100644
--- a/src/matchers/toBeExtensible.ts
+++ b/src/matchers/toBeExtensible.ts
@@ -1,5 +1,5 @@
 export function toBeExtensible(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, printExpected, printReceived } = this.utils;
 
   const pass = Object.isExtensible(actual);
diff --git a/src/matchers/toBeFalse.ts b/src/matchers/toBeFalse.ts
index 47da491..073168a 100644
--- a/src/matchers/toBeFalse.ts
+++ b/src/matchers/toBeFalse.ts
@@ -1,5 +1,5 @@
 export function toBeFalse(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint, printExpected } = this.utils;
 
   const pass = actual === false;
diff --git a/src/matchers/toBeFinite.ts b/src/matchers/toBeFinite.ts
index 2426103..f87bdda 100644
--- a/src/matchers/toBeFinite.ts
+++ b/src/matchers/toBeFinite.ts
@@ -1,5 +1,5 @@
 export function toBeFinite(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = Number.isFinite(actual);
diff --git a/src/matchers/toBeFrozen.ts b/src/matchers/toBeFrozen.ts
index 2a2819f..0ebc0a3 100644
--- a/src/matchers/toBeFrozen.ts
+++ b/src/matchers/toBeFrozen.ts
@@ -1,5 +1,5 @@
 export function toBeFrozen(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint } = this.utils;
 
   const pass = Object.isFrozen(actual);
diff --git a/src/matchers/toBeFunction.ts b/src/matchers/toBeFunction.ts
index e558c0a..fe37eae 100644
--- a/src/matchers/toBeFunction.ts
+++ b/src/matchers/toBeFunction.ts
@@ -1,5 +1,5 @@
 export function toBeFunction(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = typeof actual === 'function';
diff --git a/src/matchers/toBeHexadecimal.ts b/src/matchers/toBeHexadecimal.ts
index 6e40da7..4176195 100644
--- a/src/matchers/toBeHexadecimal.ts
+++ b/src/matchers/toBeHexadecimal.ts
@@ -1,5 +1,5 @@
 export function toBeHexadecimal(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass =
diff --git a/src/matchers/toBeInRange.ts b/src/matchers/toBeInRange.ts
index e0b5b9f..8be5c20 100644
--- a/src/matchers/toBeInRange.ts
+++ b/src/matchers/toBeInRange.ts
@@ -1,5 +1,5 @@
 export function toBeInRange(actual: unknown[], min: number, max: number) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const element = actual.find((option: any) => option < min || option >= max);
diff --git a/src/matchers/toBeInteger.ts b/src/matchers/toBeInteger.ts
index 7a47f08..365c196 100644
--- a/src/matchers/toBeInteger.ts
+++ b/src/matchers/toBeInteger.ts
@@ -1,5 +1,5 @@
 export function toBeInteger(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = isNumber(actual) && isInteger(actual);
diff --git a/src/matchers/toBeNaN.ts b/src/matchers/toBeNaN.ts
index f003fe3..5dea119 100644
--- a/src/matchers/toBeNaN.ts
+++ b/src/matchers/toBeNaN.ts
@@ -1,5 +1,5 @@
 export function toBeNaN(actual: any) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = isNaN(actual);
diff --git a/src/matchers/toBeNegative.ts b/src/matchers/toBeNegative.ts
index c761a41..c77ffb9 100644
--- a/src/matchers/toBeNegative.ts
+++ b/src/matchers/toBeNegative.ts
@@ -1,5 +1,5 @@
 export function toBeNegative(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = isNumber(actual) && isNegative(actual);
diff --git a/src/matchers/toBeNil.ts b/src/matchers/toBeNil.ts
index ec61046..327dfe3 100644
--- a/src/matchers/toBeNil.ts
+++ b/src/matchers/toBeNil.ts
@@ -1,5 +1,5 @@
 export function toBeNil(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = actual === undefined || actual === null;
diff --git a/src/matchers/toBeNumber.ts b/src/matchers/toBeNumber.ts
index d76f6bb..ae9c49c 100644
--- a/src/matchers/toBeNumber.ts
+++ b/src/matchers/toBeNumber.ts
@@ -1,5 +1,5 @@
 export function toBeNumber(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = typeof actual === 'number';
diff --git a/src/matchers/toBeObject.ts b/src/matchers/toBeObject.ts
index a93f995..0e09b3f 100644
--- a/src/matchers/toBeObject.ts
+++ b/src/matchers/toBeObject.ts
@@ -1,5 +1,5 @@
 export function toBeObject(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = typeof actual === 'object' && actual !== null && !Array.isArray(actual);
diff --git a/src/matchers/toBeOdd.ts b/src/matchers/toBeOdd.ts
index e9c4b9e..d6bf0ee 100644
--- a/src/matchers/toBeOdd.ts
+++ b/src/matchers/toBeOdd.ts
@@ -1,5 +1,5 @@
 export function toBeOdd(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = isNumber(actual) && isOdd(actual);
diff --git a/src/matchers/toBeOneOf.ts b/src/matchers/toBeOneOf.ts
index ff300ab..0d8734b 100644
--- a/src/matchers/toBeOneOf.ts
+++ b/src/matchers/toBeOneOf.ts
@@ -1,10 +1,10 @@
 import { contains } from 'src/utils';
 
 export function toBeOneOf<E = unknown>(actual: unknown, expected: readonly E[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.equals
   const pass = contains(this.equals, expected, actual);
 
   return {
diff --git a/src/matchers/toBePositive.ts b/src/matchers/toBePositive.ts
index 58a127f..3094d6c 100644
--- a/src/matchers/toBePositive.ts
+++ b/src/matchers/toBePositive.ts
@@ -1,5 +1,5 @@
 export function toBePositive(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = isNumber(actual) && isPositive(actual);
diff --git a/src/matchers/toBeSealed.ts b/src/matchers/toBeSealed.ts
index 843c611..3eb4083 100644
--- a/src/matchers/toBeSealed.ts
+++ b/src/matchers/toBeSealed.ts
@@ -1,5 +1,5 @@
 export function toBeSealed(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint } = this.utils;
 
   const pass = Object.isSealed(actual);
diff --git a/src/matchers/toBeString.ts b/src/matchers/toBeString.ts
index 0426459..6684ad5 100644
--- a/src/matchers/toBeString.ts
+++ b/src/matchers/toBeString.ts
@@ -1,5 +1,5 @@
 export function toBeString(expected: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = typeof expected === 'string' || expected instanceof String;
diff --git a/src/matchers/toBeSymbol.ts b/src/matchers/toBeSymbol.ts
index bec5fd5..dd99420 100644
--- a/src/matchers/toBeSymbol.ts
+++ b/src/matchers/toBeSymbol.ts
@@ -1,5 +1,5 @@
 export function toBeSymbol(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = typeof actual === 'symbol';
diff --git a/src/matchers/toBeTrue.ts b/src/matchers/toBeTrue.ts
index 4dfbc69..2c57e04 100644
--- a/src/matchers/toBeTrue.ts
+++ b/src/matchers/toBeTrue.ts
@@ -1,5 +1,5 @@
 export function toBeTrue(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = actual === true;
diff --git a/src/matchers/toBeValidDate.ts b/src/matchers/toBeValidDate.ts
index 44a332c..2e89485 100644
--- a/src/matchers/toBeValidDate.ts
+++ b/src/matchers/toBeValidDate.ts
@@ -1,5 +1,5 @@
 export function toBeValidDate(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, matcherHint } = this.utils;
 
   const pass = actual instanceof Date && !isNaN(actual.getTime());
diff --git a/src/matchers/toBeWithin.ts b/src/matchers/toBeWithin.ts
index ae3eec1..b2d9468 100644
--- a/src/matchers/toBeWithin.ts
+++ b/src/matchers/toBeWithin.ts
@@ -1,5 +1,5 @@
 export function toBeWithin(actual: unknown, start: number, end: number) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = typeof actual === 'number' && actual >= start && actual < end;
diff --git a/src/matchers/toChange.ts b/src/matchers/toChange.ts
new file mode 100644
index 0000000..1b91f85
--- /dev/null
+++ b/src/matchers/toChange.ts
@@ -0,0 +1,23 @@
+/**
+ * Use `.toChange` when checking if a value has changed.
+ * @example
+ * expect(() => value--).toChange(() => value);
+ */
+export function toChange(mutator: () => unknown | void, checker: () => number) {
+  // @ts-expect-error OK to have implicit any for this.utils
+  const { printReceived: print, matcherHint: hint } = this.utils;
+
+  const before = checker();
+  mutator();
+  const received = checker();
+
+  const passMessage = `
+  ${hint('.not.toChange', 'received', '')}\n\nExpected value to not change, received:\n  ${print(received)}
+  `.trim();
+  const failMessage = `
+  ${hint('.toChange', 'received', '')}\n\nExpected value to change, received:\n  ${print(received)}
+  `.trim();
+
+  const pass = received !== before;
+  return { pass, message: () => (pass ? passMessage : failMessage) };
+}
diff --git a/src/matchers/toChangeBy.ts b/src/matchers/toChangeBy.ts
new file mode 100644
index 0000000..ffd808c
--- /dev/null
+++ b/src/matchers/toChangeBy.ts
@@ -0,0 +1,23 @@
+/**
+ * Use `.toChangeBy` when checking if a value changed by an amount.
+ * @example
+ * expect(() => value--).toChangeBy(() => value, -1);
+ */
+export function toChangeBy(mutator: () => unknown | void, checker: () => number, by: number = 1) {
+  // @ts-expect-error OK to have implicit any for this.utils
+  const { printReceived: print, matcherHint: hint } = this.utils;
+
+  const before = checker();
+  mutator();
+  const received = checker() - before;
+
+  const passMessage = `
+${hint('.not.toChangeBy', 'received', '')}\n\nExpected value to not change by ${by}, received:\n  ${print(received)}
+`.trim();
+  const failMessage = `
+${hint('.toChangeBy', 'received', '')}\n\nExpected value to change by ${by}, received:\n  ${print(received)}
+`.trim();
+
+  const pass = received === by;
+  return { pass, message: () => (pass ? passMessage : failMessage) };
+}
diff --git a/src/matchers/toChangeTo.ts b/src/matchers/toChangeTo.ts
new file mode 100644
index 0000000..3a6d506
--- /dev/null
+++ b/src/matchers/toChangeTo.ts
@@ -0,0 +1,33 @@
+/**
+ * Use `.toChangeTo` when checking if a value changed to a specific value.
+ * @example
+ * expect(() => Model.deleteAll()).toChangeTo(() => Model.count(), 0);
+ */
+export function toChangeTo(mutator: () => unknown | void, checker: () => number, to: number) {
+  // @ts-expect-error OK to have implicit any for this.utils
+  const { printReceived: print, matcherHint: hint } = this.utils;
+
+  const before = checker();
+
+  if (before === to) {
+    const noChangeMessage = `${hint(
+      '.toChangeTo',
+      'received',
+      '',
+    )}\n\nCannot expect a value to change to its original state, received:  ${print(before)}`;
+    return { pass: false, message: () => noChangeMessage };
+  }
+
+  mutator();
+  const received = checker();
+
+  const passMessage = `
+${hint('.not.toChangeTo', 'received', '')}\n\nExpected value to not change to ${to}, received:\n  ${print(received)}
+`.trim();
+  const failMessage = `
+${hint('.toChangeto', 'received', '')}\n\nExpected value to change to ${to}, received:\n  ${print(received)}
+`.trim();
+
+  const pass = received !== before && received === to;
+  return { pass, message: () => (pass ? passMessage : failMessage) };
+}
diff --git a/src/matchers/toContainAllEntries.ts b/src/matchers/toContainAllEntries.ts
index b336a99..729d270 100644
--- a/src/matchers/toContainAllEntries.ts
+++ b/src/matchers/toContainAllEntries.ts
@@ -7,7 +7,7 @@ export function toContainAllEntries<E = unknown>(
   pass: boolean;
   message: () => string;
 } {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass =
diff --git a/src/matchers/toContainAllKeys.ts b/src/matchers/toContainAllKeys.ts
index f4bf2cd..f610162 100644
--- a/src/matchers/toContainAllKeys.ts
+++ b/src/matchers/toContainAllKeys.ts
@@ -1,13 +1,13 @@
 import { contains } from 'src/utils';
 
 export function toContainAllKeys<E = unknown>(actual: unknown, expected: readonly (keyof E | string)[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printExpected, printReceived, matcherHint } = this.utils;
 
   let pass = false;
   if (typeof actual === 'object' && actual !== null && !Array.isArray(actual)) {
     const objectKeys = Object.keys(actual as Record<string, unknown>);
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.equals
     pass = objectKeys.length === expected.length && expected.every(key => contains(this.equals, objectKeys, key));
   }
 
diff --git a/src/matchers/toContainAllValues.ts b/src/matchers/toContainAllValues.ts
index 82246c9..d4843d9 100644
--- a/src/matchers/toContainAllValues.ts
+++ b/src/matchers/toContainAllValues.ts
@@ -1,14 +1,14 @@
 import { contains } from 'src/utils';
 
 export function toContainAllValues<E = unknown>(actual: unknown, expected: readonly E[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   let pass = false;
   if (typeof actual === 'object' && actual !== null && !Array.isArray(actual)) {
     const values = Object.keys(actual as Record<string, unknown>).map(k => (actual as Record<string, unknown>)[k]);
     pass =
-      // @ts-expect-error OK to have implicit any for this
+      // @ts-expect-error OK to have implicit any for this.equals
       values.length === expected.length && values.every(objectValue => contains(this.equals, expected, objectValue));
   }
 
diff --git a/src/matchers/toContainAnyEntries.ts b/src/matchers/toContainAnyEntries.ts
index 84c4785..3767b4a 100644
--- a/src/matchers/toContainAnyEntries.ts
+++ b/src/matchers/toContainAnyEntries.ts
@@ -4,7 +4,7 @@ export function toContainAnyEntries<E = unknown>(
   actual: unknown,
   expected: readonly (readonly [keyof E, E[keyof E]])[],
 ) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   let pass = false;
@@ -13,7 +13,7 @@ export function toContainAnyEntries<E = unknown>(
       k,
       (actual as Record<string, unknown>)[k],
     ]);
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.equals
     pass = expected.some(entry => contains(this.equals, entries, entry));
   }
 
diff --git a/src/matchers/toContainAnyKeys.ts b/src/matchers/toContainAnyKeys.ts
index 0adb6a9..ce3123d 100644
--- a/src/matchers/toContainAnyKeys.ts
+++ b/src/matchers/toContainAnyKeys.ts
@@ -1,5 +1,5 @@
 export function toContainAnyKeys<E = unknown>(actual: unknown, expected: readonly (keyof E | string)[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = expected.some(key => Object.prototype.hasOwnProperty.call(actual, key));
diff --git a/src/matchers/toContainAnyValues.ts b/src/matchers/toContainAnyValues.ts
index 02dc094..2124c81 100644
--- a/src/matchers/toContainAnyValues.ts
+++ b/src/matchers/toContainAnyValues.ts
@@ -1,7 +1,7 @@
 import { contains } from 'src/utils';
 
 export function toContainAnyValues<E = unknown>(actual: unknown, expected: readonly E[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   let pass = false;
@@ -9,7 +9,7 @@ export function toContainAnyValues<E = unknown>(actual: unknown, expected: reado
     const objectValues = Object.keys(actual as Record<string, unknown>).map(
       k => (actual as Record<string, unknown>)[k],
     );
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.equals
     pass = expected.some(value => contains(this.equals, objectValues, value));
   }
 
diff --git a/src/matchers/toContainEntries.ts b/src/matchers/toContainEntries.ts
index 0416aa6..d3d8da8 100644
--- a/src/matchers/toContainEntries.ts
+++ b/src/matchers/toContainEntries.ts
@@ -1,7 +1,7 @@
 import { containsEntry } from 'src/utils';
 
 export function toContainEntries<E = unknown>(actual: unknown, expected: readonly (readonly [keyof E, E[keyof E]])[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   // @ts-expect-error containsEntry takes an any type
diff --git a/src/matchers/toContainEntry.ts b/src/matchers/toContainEntry.ts
index f46b0aa..fdfc1e8 100644
--- a/src/matchers/toContainEntry.ts
+++ b/src/matchers/toContainEntry.ts
@@ -1,7 +1,7 @@
 import { containsEntry } from 'src/utils';
 
 export function toContainEntry<E = unknown>(actual: unknown, expected: readonly [keyof E, E[keyof E]]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   // @ts-expect-error containsEntry takes an any type
diff --git a/src/matchers/toContainKey.ts b/src/matchers/toContainKey.ts
index da14186..61ee7fe 100644
--- a/src/matchers/toContainKey.ts
+++ b/src/matchers/toContainKey.ts
@@ -1,5 +1,5 @@
 export function toContainKey<E = unknown>(actual: unknown, expected: keyof E | string) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass =
diff --git a/src/matchers/toContainKeys.ts b/src/matchers/toContainKeys.ts
index bc0893f..6dfcdcd 100644
--- a/src/matchers/toContainKeys.ts
+++ b/src/matchers/toContainKeys.ts
@@ -1,5 +1,5 @@
 export function toContainKeys<E = unknown>(actual: unknown, expected: readonly (keyof E | string)[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = expected.every(
diff --git a/src/matchers/toContainValue.ts b/src/matchers/toContainValue.ts
index 80434dd..ddf5c30 100644
--- a/src/matchers/toContainValue.ts
+++ b/src/matchers/toContainValue.ts
@@ -1,13 +1,13 @@
 import { contains } from 'src/utils';
 
 export function toContainValue<E = unknown>(actual: unknown, expected: E) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   let pass = false;
   if (typeof actual === 'object' && actual !== null && !Array.isArray(actual)) {
     const values = Object.keys(actual as Record<string, unknown>).map(k => (actual as Record<string, unknown>)[k]);
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.equals
     pass = contains(this.equals, values, expected);
   }
 
diff --git a/src/matchers/toContainValues.ts b/src/matchers/toContainValues.ts
index c1c3691..5d12bbc 100644
--- a/src/matchers/toContainValues.ts
+++ b/src/matchers/toContainValues.ts
@@ -1,13 +1,13 @@
 import { contains } from 'src/utils';
 
 export function toContainValues<E = unknown>(actual: unknown, expected: readonly E[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   let pass = false;
   if (typeof actual === 'object' && actual !== null && !Array.isArray(actual)) {
     const values = Object.keys(actual as Record<string, unknown>).map(k => (actual as Record<string, unknown>)[k]);
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.equals
     pass = expected.every(value => contains(this.equals, values, value));
   }
 
diff --git a/src/matchers/toEndWith.ts b/src/matchers/toEndWith.ts
index d3cc1a6..ac521f2 100644
--- a/src/matchers/toEndWith.ts
+++ b/src/matchers/toEndWith.ts
@@ -1,5 +1,5 @@
 export function toEndWith(actual: unknown, expected: string) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = typeof actual === 'string' && actual.endsWith(expected);
diff --git a/src/matchers/toEqualCaseInsensitive.ts b/src/matchers/toEqualCaseInsensitive.ts
index c1f8261..cc1ce5b 100644
--- a/src/matchers/toEqualCaseInsensitive.ts
+++ b/src/matchers/toEqualCaseInsensitive.ts
@@ -1,5 +1,5 @@
 export function toEqualCaseInsensitive(actual: unknown, expected: string) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = String(actual).toLowerCase() === String(expected).toLowerCase();
diff --git a/src/matchers/toEqualIgnoringWhitespace.ts b/src/matchers/toEqualIgnoringWhitespace.ts
index bff5943..fb289ae 100644
--- a/src/matchers/toEqualIgnoringWhitespace.ts
+++ b/src/matchers/toEqualIgnoringWhitespace.ts
@@ -17,7 +17,7 @@ const getDiff = (received: string, expected: string) => {
 };
 
 export function toEqualIgnoringWhitespace(actual: unknown, expected: string) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint, EXPECTED_COLOR } = this.utils;
 
   /* determine whether strings are equal after removing white-space */
@@ -38,9 +38,9 @@ export function toEqualIgnoringWhitespace(actual: unknown, expected: string) {
             matcherHint('.toEqualIgnoringWhitespace') +
             '\n\n' +
             'Expected values to be equal while ignoring white-space (using ===):\n' +
-            // @ts-expect-error OK to have implicit any for this
+            // @ts-expect-error OK to have implicit any for this.utils
             `Expected:\n  ${printExpected(this.utils, diff)}\n\n` +
-            // @ts-expect-error OK to have implicit any for this
+            // @ts-expect-error OK to have implicit any for this.utils
             `Received:\n  ${printReceived(this.utils, diff)}`
           );
         },
diff --git a/src/matchers/toHaveBeenCalledAfter.ts b/src/matchers/toHaveBeenCalledAfter.ts
index 0c26d7d..8ea5bfa 100644
--- a/src/matchers/toHaveBeenCalledAfter.ts
+++ b/src/matchers/toHaveBeenCalledAfter.ts
@@ -5,16 +5,16 @@ export function toHaveBeenCalledAfter(
   expected: jest.MockInstance<any, any[]>,
   failIfNoFirstInvocation: boolean = true,
 ) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   if (!isJestMockOrSpy(actual)) {
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.utils
     return { pass: false, message: mockCheckFailMessage(this.utils, actual, true) };
   }
 
   if (!isJestMockOrSpy(expected)) {
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.utils
     return { pass: false, message: mockCheckFailMessage(this.utils, expected, false) };
   }
 
diff --git a/src/matchers/toHaveBeenCalledBefore.ts b/src/matchers/toHaveBeenCalledBefore.ts
index d08f221..aa6c657 100644
--- a/src/matchers/toHaveBeenCalledBefore.ts
+++ b/src/matchers/toHaveBeenCalledBefore.ts
@@ -5,16 +5,16 @@ export function toHaveBeenCalledBefore(
   expected: jest.MockInstance<any, any[]>,
   failIfNoSecondInvocation: boolean = true,
 ) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   if (!isJestMockOrSpy(actual)) {
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.utils
     return { pass: false, message: mockCheckFailMessage(this.utils, actual, true) };
   }
 
   if (!isJestMockOrSpy(expected)) {
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.utils
     return { pass: false, message: mockCheckFailMessage(this.utils, expected, false) };
   }
 
diff --git a/src/matchers/toHaveBeenCalledExactlyOnceWith.ts b/src/matchers/toHaveBeenCalledExactlyOnceWith.ts
index 3079a1f..8915458 100644
--- a/src/matchers/toHaveBeenCalledExactlyOnceWith.ts
+++ b/src/matchers/toHaveBeenCalledExactlyOnceWith.ts
@@ -1,7 +1,7 @@
 import { isJestMockOrSpy } from 'src/utils';
 
 export function toHaveBeenCalledExactlyOnceWith(received: unknown, ...expected: unknown[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, printWithType, matcherHint } = this.utils;
 
   if (!isJestMockOrSpy(received)) {
@@ -20,7 +20,7 @@ export function toHaveBeenCalledExactlyOnceWith(received: unknown, ...expected:
   const actual = received.mock.calls[0];
   // @ts-expect-error isJestMockOrSpy provides the type check
   const invokedOnce = received.mock.calls.length === 1;
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.equals
   const pass = invokedOnce && this.equals(expected, actual);
 
   return {
diff --git a/src/matchers/toHaveBeenCalledOnce.ts b/src/matchers/toHaveBeenCalledOnce.ts
index 08d7870..a3cb697 100644
--- a/src/matchers/toHaveBeenCalledOnce.ts
+++ b/src/matchers/toHaveBeenCalledOnce.ts
@@ -1,7 +1,7 @@
 import { isJestMockOrSpy } from 'src/utils';
 
 export function toHaveBeenCalledOnce(actual: unknown) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printWithType, matcherHint } = this.utils;
 
   if (!isJestMockOrSpy(actual)) {
diff --git a/src/matchers/toInclude.ts b/src/matchers/toInclude.ts
index 23eb90b..a1c567c 100644
--- a/src/matchers/toInclude.ts
+++ b/src/matchers/toInclude.ts
@@ -1,5 +1,5 @@
 export function toInclude(actual: unknown, expected: string) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = typeof actual === 'string' && actual.includes(expected);
diff --git a/src/matchers/toIncludeAllMembers.ts b/src/matchers/toIncludeAllMembers.ts
index c098f4b..33dfa7b 100644
--- a/src/matchers/toIncludeAllMembers.ts
+++ b/src/matchers/toIncludeAllMembers.ts
@@ -1,11 +1,11 @@
 import { contains } from 'src/utils';
 
 export function toIncludeAllMembers<E = unknown>(actual: unknown[], expected: readonly E[] | E) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass =
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.equals
     Array.isArray(actual) && Array.isArray(expected) && expected.every(val => contains(this.equals, actual, val));
 
   return {
diff --git a/src/matchers/toIncludeAllPartialMembers.ts b/src/matchers/toIncludeAllPartialMembers.ts
index 1437e03..b0c1cd2 100644
--- a/src/matchers/toIncludeAllPartialMembers.ts
+++ b/src/matchers/toIncludeAllPartialMembers.ts
@@ -1,14 +1,14 @@
 import { containsEntry } from 'src/utils';
 
 export function toIncludeAllPartialMembers<E = unknown>(actual: unknown, expected: readonly E[] | E) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass =
     Array.isArray(actual) &&
     Array.isArray(expected) &&
     expected.every(partial =>
-      // @ts-expect-error OK to have implicit any for this
+      // @ts-expect-error OK to have implicit any for this.equals
       actual.some(value => Object.entries(partial).every(entry => containsEntry(this.equals, value, entry))),
     );
 
diff --git a/src/matchers/toIncludeAnyMembers.ts b/src/matchers/toIncludeAnyMembers.ts
index 125bd6e..d51defc 100644
--- a/src/matchers/toIncludeAnyMembers.ts
+++ b/src/matchers/toIncludeAnyMembers.ts
@@ -1,11 +1,11 @@
 import { contains } from 'src/utils';
 
 export function toIncludeAnyMembers<E = unknown>(actual: unknown, expected: readonly E[] | E) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass =
-    // @ts-expect-error OK to have implicit any for this
+    // @ts-expect-error OK to have implicit any for this.equals
     Array.isArray(actual) && Array.isArray(expected) && expected.some(member => contains(this.equals, actual, member));
 
   return {
diff --git a/src/matchers/toIncludeMultiple.ts b/src/matchers/toIncludeMultiple.ts
index 9461ef2..8623567 100644
--- a/src/matchers/toIncludeMultiple.ts
+++ b/src/matchers/toIncludeMultiple.ts
@@ -1,5 +1,5 @@
 export function toIncludeMultiple(actual: unknown, expected: readonly string[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = typeof actual === 'string' && expected.every(value => actual.includes(value));
diff --git a/src/matchers/toIncludeRepeated.ts b/src/matchers/toIncludeRepeated.ts
index 9823018..80fbbd6 100644
--- a/src/matchers/toIncludeRepeated.ts
+++ b/src/matchers/toIncludeRepeated.ts
@@ -1,5 +1,5 @@
 export function toIncludeRepeated(actual: unknown, expected: string, occurrences: number) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = typeof actual === 'string' && (actual.match(new RegExp(expected, 'g')) || []).length === occurrences;
diff --git a/src/matchers/toIncludeSameMembers.ts b/src/matchers/toIncludeSameMembers.ts
index 1e32c45..162333e 100644
--- a/src/matchers/toIncludeSameMembers.ts
+++ b/src/matchers/toIncludeSameMembers.ts
@@ -1,8 +1,8 @@
 export function toIncludeSameMembers<E = unknown>(actual: unknown, expected: readonly E[]) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.equals
   const pass = predicate(this.equals, actual, expected);
 
   return {
diff --git a/src/matchers/toIncludeSamePartialMembers.ts b/src/matchers/toIncludeSamePartialMembers.ts
index e95510b..0863340 100644
--- a/src/matchers/toIncludeSamePartialMembers.ts
+++ b/src/matchers/toIncludeSamePartialMembers.ts
@@ -1,10 +1,10 @@
 import { containsEntry } from '../utils';
 
 export function toIncludeSamePartialMembers<E = unknown>(actual: unknown, expected: E) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.equals
   const pass = predicate(this.equals, actual, expected);
 
   return {
diff --git a/src/matchers/toPartiallyContain.ts b/src/matchers/toPartiallyContain.ts
index 43d91f4..8d85f48 100644
--- a/src/matchers/toPartiallyContain.ts
+++ b/src/matchers/toPartiallyContain.ts
@@ -1,14 +1,14 @@
 import { containsEntry } from 'src/utils';
 
 export function toPartiallyContain<E = unknown>(actual: unknown, expected: E) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass =
     Array.isArray(actual) &&
     Array.isArray([expected]) &&
     [expected].every(partial =>
-      // @ts-expect-error OK to have implicit any for this
+      // @ts-expect-error OK to have implicit any for this.equals
       actual.some(value => Object.entries(partial).every(entry => containsEntry(this.equals, value, entry))),
     );
 
diff --git a/src/matchers/toReject.ts b/src/matchers/toReject.ts
index 1fde8df..4b8225e 100644
--- a/src/matchers/toReject.ts
+++ b/src/matchers/toReject.ts
@@ -1,5 +1,5 @@
 export async function toReject(actual: Promise<unknown>) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint } = this.utils;
 
   const pass = await actual.then(
diff --git a/src/matchers/toResolve.ts b/src/matchers/toResolve.ts
index 5c267d2..b1b7934 100644
--- a/src/matchers/toResolve.ts
+++ b/src/matchers/toResolve.ts
@@ -1,5 +1,5 @@
 export async function toResolve(actual: Promise<unknown>) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { matcherHint } = this.utils;
 
   const pass = await actual.then(
diff --git a/src/matchers/toSatisfy.ts b/src/matchers/toSatisfy.ts
index ebb9c29..f414024 100644
--- a/src/matchers/toSatisfy.ts
+++ b/src/matchers/toSatisfy.ts
@@ -1,5 +1,5 @@
 export function toSatisfy<E = any>(actual: E, expected: (x: E) => boolean) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = expected(actual);
diff --git a/src/matchers/toSatisfyAll.ts b/src/matchers/toSatisfyAll.ts
index ebb59a6..1b87a67 100644
--- a/src/matchers/toSatisfyAll.ts
+++ b/src/matchers/toSatisfyAll.ts
@@ -1,5 +1,5 @@
 export function toSatisfyAll<E = unknown>(actual: E[], expected: (x: E) => boolean) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = actual.every(expected);
diff --git a/src/matchers/toSatisfyAny.ts b/src/matchers/toSatisfyAny.ts
index cfa3d81..0ce5880 100644
--- a/src/matchers/toSatisfyAny.ts
+++ b/src/matchers/toSatisfyAny.ts
@@ -1,5 +1,5 @@
 export function toSatisfyAny(actual: unknown, expected: (x: unknown) => boolean) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   let pass = false;
diff --git a/src/matchers/toStartWith.ts b/src/matchers/toStartWith.ts
index 0d52ecc..ca52bb4 100644
--- a/src/matchers/toStartWith.ts
+++ b/src/matchers/toStartWith.ts
@@ -1,5 +1,5 @@
 export function toStartWith(actual: unknown, expected: string) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const { printReceived, printExpected, matcherHint } = this.utils;
 
   const pass = typeof actual === 'string' && actual.startsWith(expected);
diff --git a/src/matchers/toThrowWithMessage.ts b/src/matchers/toThrowWithMessage.ts
index bc5147c..24e205e 100644
--- a/src/matchers/toThrowWithMessage.ts
+++ b/src/matchers/toThrowWithMessage.ts
@@ -49,9 +49,9 @@ export function toThrowWithMessage(
     | ((...args: any[]) => { message: string }),
   message: string | RegExp,
 ) {
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.utils
   const utils = this.utils;
-  // @ts-expect-error OK to have implicit any for this
+  // @ts-expect-error OK to have implicit any for this.promise
   const isFromReject = this && this.promise === 'rejects'; // See https://github.com/facebook/jest/pull/7621#issue-244312550
   if ((!callbackOrPromiseReturn || typeof callbackOrPromiseReturn !== 'function') && !isFromReject) {
     return {
diff --git a/types/index.d.ts b/types/index.d.ts
index a6908dc..e7e4a6c 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -667,6 +667,27 @@ declare namespace jest {
      */
     toBeObject(): R;
 
+    /**
+     * Use `.toChange` when checking if a value has changed.
+     * @example
+     * expect(() => value--).toChange(() => value);
+     */
+    toChange<E = unknown>(checker: () => E): R;
+
+    /**
+     * Use `.toChangeTo` when checking if a value changed to a specific value.
+     * @example
+     * expect(() => Model.deleteAll()).toChangeTo(() => Model.count(), 0);
+     */
+    toChangeTo<E = unknown>(checker: () => E, to: E): R;
+
+    /**
+     * Use `.toChangeBy` when checking if a value changed by an amount.
+     * @example
+     * expect(() => value--).toChangeBy(() => value, -1);
+     */
+    toChangeBy(checker: () => number, by?: number): R;
+
     /**
      * Use `.toContainKey` when checking if an object contains the provided key.
      *
diff --git a/website/docs/getting-started/setup.md b/website/docs/getting-started/setup.md
index e09e7a0..3a520d7 100644
--- a/website/docs/getting-started/setup.md
+++ b/website/docs/getting-started/setup.md
@@ -69,7 +69,7 @@ import 'vitest';
 declare module 'vitest' {
   interface Assertion<T = any> extends CustomMatchers<T> {}
   interface AsymmetricMatchersContaining<T = any> extends CustomMatchers<T> {}
-  interface ExpectStatic extends CustomMatchers<T> {}
+  interface ExpectStatic<T = any> extends CustomMatchers<T> {}
 }
 ```
 
@@ -99,7 +99,7 @@ import 'vi';
 declare module 'vi' {
   interface Assertion<T = any> extends CustomMatchers<T> {}
   interface AsymmetricMatchersContaining<T = any> extends CustomMatchers<T> {}
-  interface ExpectStatic extends CustomMatchers<T> {}
+  interface ExpectStatic<T = any> extends CustomMatchers<T> {}
 }
 ```
 
diff --git a/website/docs/matchers/function.mdx b/website/docs/matchers/function.mdx
index 1b24023..cdb5a4d 100644
--- a/website/docs/matchers/function.mdx
+++ b/website/docs/matchers/function.mdx
@@ -15,6 +15,45 @@ Use `.toBeFunction` when checking if a value is a `Function`.
 });`}
 </TestFile>
 
+### .toChange(checker)
+
+Use `.toChange` when checking if the callback function mutates a piece of state that is queried by the checker function.
+
+<TestFile name="toChange">
+  {`
+test('passes when given a value that the mutator increments', () => {
+  let value = 0;
+  expect(() => value++).toChange(() => value);
+});
+  `.trim()}
+</TestFile>
+
+### .toChangeBy(checker, by)
+
+Use `.toChangeBy` when checking if the callback function mutates a piece of state by a specific amount.
+
+<TestFile name="toChangeBy">
+  {`
+test('passes when given a value that the mutator decrements', () => {
+  let value = 1;
+  expect(() => value--).toChangeBy(() => value, -1);
+});
+  `.trim()}
+</TestFile>
+
+### .toChangeTo(checker, to)
+
+Use `.toChangeTo` when checking if the callback function mutates a piece of state to a target value.
+
+<TestFile name="toChangeTo">
+  {`
+test('passes when the value being checked becomes the provided value', () => {
+  let value = 1;
+  expect(() => value = 10).toChangeTo(() => value, 10);
+});
+  `.trim()}
+</TestFile>
+
 ### .toThrowWithMessage(type, message)
 
 Use `.toThrowWithMessage` when checking if a callback function throws an error with a given error type and given error message. Message can either be a `String` or a `RegExp`.
diff --git a/website/docs/matchers/index.md b/website/docs/matchers/index.md
index e1130ff..f5c9516 100644
--- a/website/docs/matchers/index.md
+++ b/website/docs/matchers/index.md
@@ -44,6 +44,9 @@ sidebar_position: 1
 ## [Function](/docs/matchers/function)
 
 - [.toBeFunction()](/docs/matchers/function/#tobefunction)
+- [.toChange()](/docs/matchers/function/#tochange)
+- [.toChangeBy()](/docs/matchers/function/#tochangeby)
+- [.toChangeTo()](/docs/matchers/function/#tochangeto)
 - [.toThrowWithMessage()](/docs/matchers/function/#tothrowwithmessagetype-message)
 
 ## [Mock](/docs/matchers/mock)



Test patch:
diff --git a/test/fixtures/counter.ts b/test/fixtures/counter.ts
new file mode 100644
index 0000000..4c7dfc0
--- /dev/null
+++ b/test/fixtures/counter.ts
@@ -0,0 +1,22 @@
+export class Counter {
+  private value: number;
+
+  constructor() {
+    this.increment = this.increment.bind(this);
+    this.count = this.count.bind(this);
+    this.reset = this.reset.bind(this);
+    this.value = 0;
+  }
+
+  increment() {
+    this.value++;
+  }
+
+  count() {
+    return this.value;
+  }
+
+  reset() {
+    this.value = 0;
+  }
+}
diff --git a/test/matchers/__snapshots__/toChange.test.ts.snap b/test/matchers/__snapshots__/toChange.test.ts.snap
new file mode 100644
index 0000000..1a73fee
--- /dev/null
+++ b/test/matchers/__snapshots__/toChange.test.ts.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`.not.toChange fails when a value expected not to change defies all expectations and changes 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).not.toChange()</intensity>
+
+Expected value to not change, received:
+  <red>1</color>"
+`;
+
+exports[`.not.toChange fails when mutating a counter 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).not.toChange()</intensity>
+
+Expected value to not change, received:
+  <red>1</color>"
+`;
+
+exports[`.toChange fails when a given value does not increment 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).toChange()</intensity>
+
+Expected value to change, received:
+  <red>0</color>"
+`;
+
+exports[`.toChange fails when not mutating a counter 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).toChange()</intensity>
+
+Expected value to change, received:
+  <red>0</color>"
+`;
diff --git a/test/matchers/__snapshots__/toChangeBy.test.ts.snap b/test/matchers/__snapshots__/toChangeBy.test.ts.snap
new file mode 100644
index 0000000..f08d5d5
--- /dev/null
+++ b/test/matchers/__snapshots__/toChangeBy.test.ts.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`.not.toChangeBy fails when a value expected not to change defies all expectations and changes 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).not.toChangeBy()</intensity>
+
+Expected value to not change by 1, received:
+  <red>1</color>"
+`;
+
+exports[`.not.toChangeBy fails when mutating a counter 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).not.toChangeBy()</intensity>
+
+Expected value to not change by 1, received:
+  <red>1</color>"
+`;
+
+exports[`.toChangeBy fails when a given value does not increment 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).toChangeBy()</intensity>
+
+Expected value to change by 1, received:
+  <red>0</color>"
+`;
+
+exports[`.toChangeBy fails when not mutating a counter 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).toChangeBy()</intensity>
+
+Expected value to change by 1, received:
+  <red>0</color>"
+`;
diff --git a/test/matchers/__snapshots__/toChangeTo.test.ts.snap b/test/matchers/__snapshots__/toChangeTo.test.ts.snap
new file mode 100644
index 0000000..da8cb13
--- /dev/null
+++ b/test/matchers/__snapshots__/toChangeTo.test.ts.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`.not.toChange fails when a value expected not to change defies all expectations and changes 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).not.toChangeTo()</intensity>
+
+Expected value to not change to 1, received:
+  <red>1</color>"
+`;
+
+exports[`.not.toChange fails when mutating a counter 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).not.toChangeTo()</intensity>
+
+Expected value to not change to 1, received:
+  <red>1</color>"
+`;
+
+exports[`.toChangeTo fails when a given value does not increment 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).toChangeto()</intensity>
+
+Expected value to change to 1, received:
+  <red>0</color>"
+`;
+
+exports[`.toChangeTo fails when not mutating a counter 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).toChangeto()</intensity>
+
+Expected value to change to 1, received:
+  <red>0</color>"
+`;
+
+exports[`.toChangeTo fails when resetting a counter back because a change cannot be the same as the initial value 1`] = `
+"<dim>expect(</intensity><red>received</color><dim>).toChangeTo()</intensity>
+
+Cannot expect a value to change to its original state, received:  <red>0</color>"
+`;
diff --git a/test/matchers/toChange.test.ts b/test/matchers/toChange.test.ts
new file mode 100644
index 0000000..1602954
--- /dev/null
+++ b/test/matchers/toChange.test.ts
@@ -0,0 +1,48 @@
+import * as matcher from 'src/matchers/toChange';
+import { Counter } from '../fixtures/counter';
+
+expect.extend(matcher);
+
+describe('.toChange', () => {
+  test('passes when given a value that the mutator increments', () => {
+    let value = 0;
+    expect(() => value++).toChange(() => value);
+  });
+
+  test('passes when mutating a counter', () => {
+    const counter = new Counter();
+    expect(counter.increment).toChange(counter.count);
+  });
+
+  test('fails when a given value does not increment', () => {
+    const value = 0;
+    expect(() => expect(() => value).toChange(() => value)).toThrowErrorMatchingSnapshot();
+  });
+
+  test('fails when not mutating a counter', () => {
+    const counter = new Counter();
+    expect(() => expect(counter.count).toChange(counter.count)).toThrowErrorMatchingSnapshot();
+  });
+});
+
+describe('.not.toChange', () => {
+  test('passes when given a mutator that does not increment the value', () => {
+    const value = 0;
+    expect(() => value).not.toChange(() => value);
+  });
+
+  test('passes when not mutating a counter', () => {
+    const counter = new Counter();
+    expect(counter.count).not.toChange(counter.count);
+  });
+
+  test('fails when mutating a counter', () => {
+    const counter = new Counter();
+    expect(() => expect(counter.increment).not.toChange(counter.count)).toThrowErrorMatchingSnapshot();
+  });
+
+  test('fails when a value expected not to change defies all expectations and changes', () => {
+    let value = 0;
+    expect(() => expect(() => value++).not.toChange(() => value)).toThrowErrorMatchingSnapshot();
+  });
+});
diff --git a/test/matchers/toChangeBy.test.ts b/test/matchers/toChangeBy.test.ts
new file mode 100644
index 0000000..2464b66
--- /dev/null
+++ b/test/matchers/toChangeBy.test.ts
@@ -0,0 +1,75 @@
+import * as matcher from 'src/matchers/toChangeBy';
+import { Counter } from '../fixtures/counter';
+
+expect.extend(matcher);
+
+describe('.toChangeBy', () => {
+  test('passes when given a value that the mutator increments', () => {
+    let value = 0;
+    expect(() => value--).toChangeBy(() => value, -1);
+  });
+
+  test('passes when mutating a counter', () => {
+    const counter = new Counter();
+    expect(counter.increment).toChangeBy(counter.count);
+  });
+
+  test('passes when resetting a counter', () => {
+    const counter = new Counter();
+    counter.increment();
+    expect(() => {
+      counter.increment();
+      counter.increment();
+    }).toChangeBy(() => counter.count(), 2);
+  });
+
+  test('fails when a given value does not increment', () => {
+    const value = 0;
+    expect(() => expect(() => value).toChangeBy(() => value)).toThrowErrorMatchingSnapshot();
+  });
+
+  test('fails when not mutating a counter', () => {
+    const counter = new Counter();
+    expect(() => expect(counter.count).toChangeBy(counter.count)).toThrowErrorMatchingSnapshot();
+  });
+
+  test('passes when resetting a counter back', () => {
+    const counter = new Counter();
+    counter.increment();
+    counter.increment();
+    counter.increment();
+    expect(() => counter.reset()).toChangeBy(() => counter.count(), -3);
+  });
+});
+
+describe('.not.toChangeBy', () => {
+  test('passes when given a mutator that does not increment the value', () => {
+    const value = 0;
+    expect(() => value).not.toChangeBy(() => value, 1);
+  });
+
+  test('passes when not mutating a counter', () => {
+    const counter = new Counter();
+    expect(counter.count).not.toChangeBy(counter.count, 1);
+  });
+
+  test('fails when resetting a counter', () => {
+    const counter = new Counter();
+    expect(() => {
+      counter.increment();
+      counter.increment();
+      counter.increment();
+      counter.reset();
+    }).not.toChangeBy(() => counter.count(), -3);
+  });
+
+  test('fails when mutating a counter', () => {
+    const counter = new Counter();
+    expect(() => expect(counter.increment).not.toChangeBy(counter.count, 1)).toThrowErrorMatchingSnapshot();
+  });
+
+  test('fails when a value expected not to change defies all expectations and changes', () => {
+    let value = 0;
+    expect(() => expect(() => value++).not.toChangeBy(() => value, 1)).toThrowErrorMatchingSnapshot();
+  });
+});
diff --git a/test/matchers/toChangeTo.test.ts b/test/matchers/toChangeTo.test.ts
new file mode 100644
index 0000000..da7d140
--- /dev/null
+++ b/test/matchers/toChangeTo.test.ts
@@ -0,0 +1,85 @@
+import * as matcher from 'src/matchers/toChangeTo';
+import { Counter } from '../fixtures/counter';
+
+expect.extend(matcher);
+
+describe('.toChangeTo', () => {
+  test('passes when given a value that the mutator increments', () => {
+    let value = 0;
+    expect(() => value++).toChangeTo(() => value, 1);
+  });
+
+  test('passes when mutating a counter', () => {
+    const counter = new Counter();
+    expect(counter.increment).toChangeTo(counter.count, 1);
+  });
+
+  test('passes when resetting a counter', () => {
+    const counter = new Counter();
+    expect(() => {
+      counter.increment();
+      counter.increment();
+      counter.increment();
+    }).toChangeTo(() => counter.count(), 3);
+  });
+
+  test('fails when a given value does not increment', () => {
+    const value = 0;
+    expect(() => expect(() => value).toChangeTo(() => value, 1)).toThrowErrorMatchingSnapshot();
+  });
+
+  test('fails when not mutating a counter', () => {
+    const counter = new Counter();
+    expect(() => expect(counter.count).toChangeTo(counter.count, 1)).toThrowErrorMatchingSnapshot();
+  });
+
+  test('fails when resetting a counter back because a change cannot be the same as the initial value', () => {
+    const counter = new Counter();
+    expect(() =>
+      expect(() => {
+        counter.increment();
+        counter.increment();
+        counter.increment();
+        counter.reset();
+      }).toChangeTo(() => counter.count(), 0),
+    ).toThrowErrorMatchingSnapshot();
+  });
+
+  test('passes when given a non-numeric value', () => {
+    let special = 'spicy chicken sandwich';
+    expect(() => {
+      special = 'meatloaf';
+    }).toChangeTo(() => special, 'meatloaf');
+  });
+});
+
+describe('.not.toChange', () => {
+  test('passes when given a mutator that does not increment the value', () => {
+    const value = 0;
+    expect(() => value).not.toChangeTo(() => value, 1);
+  });
+
+  test('passes when not mutating a counter', () => {
+    const counter = new Counter();
+    expect(counter.count).not.toChangeTo(counter.count, 1);
+  });
+
+  test('passes when resetting a counter', () => {
+    const counter = new Counter();
+    expect(() => {
+      counter.increment();
+      counter.increment();
+      counter.increment();
+    }).not.toChangeTo(() => counter.count(), 0);
+  });
+
+  test('fails when mutating a counter', () => {
+    const counter = new Counter();
+    expect(() => expect(counter.increment).not.toChangeTo(counter.count, 1)).toThrowErrorMatchingSnapshot();
+  });
+
+  test('fails when a value expected not to change defies all expectations and changes', () => {
+    let value = 0;
+    expect(() => expect(() => value++).not.toChangeTo(() => value, 1)).toThrowErrorMatchingSnapshot();
+  });
+});