jest-community / jest-extended

Additional Jest matchers 🃏💪
https://jest-extended.jestcommunity.dev/
MIT License
2.32k stars 223 forks source link

Expect a something to be change by amount #253

Open bogdan opened 4 years ago

bogdan commented 4 years ago

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:

// 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.

rgibson518 commented 4 years ago

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.

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 commented 2 years ago

Here is a more advanced version that supports any change, not change or to changes.

/**
 * 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 commented 1 year ago

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);
});