textbook / rxjest

Jest matchers for working with RxJS observables
ISC License
0 stars 0 forks source link

[Feature Request] Add parameter/matcher for expecting specific Error #1

Open adworacz opened 1 year ago

adworacz commented 1 year ago

Not sure how you'd like to implement this, but we have a lot of use in asserting that specific errors are thrown/returned in our Observables.

I don't see an obvious way on how to assert what kind of errors are thrown with .toError(). I figure either being able to specify a custom assertion callback, and/or a regex/string matcher would go a long way.

Love this project by the way, keep up the good work!

textbook commented 1 year ago

Thanks for the feedback, great to hear people are getting value from it!

You have the concrete need here; programming by wishful thinking, what would example tests look like? Existing matchers that could be good starting points for an API:

Would it be an argument to .toThrow, which would either: be mandatory, meaning a breaking API change; be optional, adding complexity when dealing with whether or not the options were supplied; or be in the supplied options (as what, matching, satisfying, ...)? Or would it be an additional matcher (called what, toErrorWith, toErrorMatching, toErrorSatisfying, ...)?

adworacz commented 1 year ago

Hmm, these are great questions!

Off the cuff, I'd say our initial needs are:

  1. Being able to match based on a specific regex pattern or message string.
  2. Being able to ensure the error is an instance of a class.

We write our own custom error classes occasionally and need to assert on their types as well as their message contents.

So something along the lines of:

await expect(throwError(() => new CustomError("oh no!"))).toError(/oh no/);
await expect(throwError(() => new CustomError("oh no!"))).toError('oh no');
await expect(throwError(() => new CustomError("oh no!"))).toError(CustomError, /oh no/);
await expect(throwError(() => new CustomError("oh no!"))).toError(CustomError, 'oh no');
await expect(throwError(() => new CustomError("oh no!"))).toError(CustomError);

(Just operating with the existing toError matcher, totally cool with a new dedicated matcher)

Another option is simply being able to access the actual error and then reuse all of the existing error matching capability provided by Jest, like:

await expect(throwError(() => new CustomError("oh no!"))).toError((e) => {
   expect(e).toBeInstanceOf(CustomError);
   expect(e).toBe(new CustomError("oh no!");
   expect(e.message).toMatch(/oh no/);
   expect(e.customProperty).toBe(foobar);
});

As for your other questions:

  1. I'd say we certainly don't need to make the parameter to toError() mandatory, so no breaking changes. Having optional args would be plenty.
  2. I think that adding adding it to the existing options seems a little complex, at least from an API standpoint.
    • I think that .toError(CustomError, /foo bar/) is a lot cleaner to write (and read) than .toError({ instanceOf: CustomError, matching: /foo bar/})
  3. I think additional matchers are perfectly acceptable here, and can have the benefit of required args to differentiate from the options, and has the benefit of not breaking backwards compatibility.
textbook commented 1 year ago

v0.7.0 has the first iteration of this: .toErrorWith, just covering the regex matcher so far.

adworacz commented 1 year ago

Awesome, thank you! I'll give it a try soon and report back any issues that I may have.

adworacz commented 1 year ago

In my local testing this is working well, thank you for adding it!

We still have a use case for deeper error object introspection, and .toBeInstanceOf() styled checks, but this is a great start!


(This might be worth it's own separate Issue, which I'm happy to cut if desired)

Speaking on the deeper error object introspection, I think this is something of a limitation with toEmit() as well. More specifically, it works well on primitive values, but it's less adaptable for more complex object types.

Effectively, we have a need to validate a set of properties on an emitted object. expect.objectContaining gets pretty far here, allowing you to do:

await expect(getObject()).toEmit(expect.objectContaining({ foo: 'baz', bar: expect.stringContaining('example')}))

However, you can't run any more complex validation on the emitted object, or mix objectContaining with a strictEquals, like so:

....toEmit(expect.objectContaining({
   foo: 'baz',
   nestedValue: expect.strictEquals({ //Doesn't work, as Jest doesn't expose this as an asymmetric operator
       a: 'b',
       c: 'd',
   }),
})

With all this in mind, being able to access the actual emitted value/object would allow for much more complex logic, something like:

...toEmit((object) => {
    expect(object.foo).toBe('baz')
    expect(object.nestedValue).toStrictEqual({...})
    // etc etc.
})
textbook commented 1 year ago

I don't think that's this library's responsibility. RxJeSt just exposes the observable data to the tests in a predictable way, if you want to run arbitrary expectations on it you might want something like .toSatisfy.