| 1 | # Prevent calling `expect` conditionally (`no-conditional-expect`)
|
|---|
| 2 |
|
|---|
| 3 | This rule prevents the use of `expect` in conditional blocks, such as `if`s &
|
|---|
| 4 | `catch`s.
|
|---|
| 5 |
|
|---|
| 6 | This includes using `expect` in callbacks to functions named `catch`, which are
|
|---|
| 7 | assumed to be promises.
|
|---|
| 8 |
|
|---|
| 9 | ## Rule Details
|
|---|
| 10 |
|
|---|
| 11 | Jest only considers a test to have failed if it throws an error, meaning if
|
|---|
| 12 | calls to assertion functions like `expect` occur in conditional code such as a
|
|---|
| 13 | `catch` statement, tests can end up passing but not actually test anything.
|
|---|
| 14 |
|
|---|
| 15 | Additionally, conditionals tend to make tests more brittle and complex, as they
|
|---|
| 16 | increase the amount of mental thinking needed to understand what is actually
|
|---|
| 17 | being tested.
|
|---|
| 18 |
|
|---|
| 19 | While `expect.assertions` & `expect.hasAssertions` can help prevent tests from
|
|---|
| 20 | silently being skipped, when combined with conditionals they typically result in
|
|---|
| 21 | even more complexity being introduced.
|
|---|
| 22 |
|
|---|
| 23 | The following patterns are warnings:
|
|---|
| 24 |
|
|---|
| 25 | ```js
|
|---|
| 26 | it('foo', () => {
|
|---|
| 27 | doTest && expect(1).toBe(2);
|
|---|
| 28 | });
|
|---|
| 29 |
|
|---|
| 30 | it('bar', () => {
|
|---|
| 31 | if (!skipTest) {
|
|---|
| 32 | expect(1).toEqual(2);
|
|---|
| 33 | }
|
|---|
| 34 | });
|
|---|
| 35 |
|
|---|
| 36 | it('baz', async () => {
|
|---|
| 37 | try {
|
|---|
| 38 | await foo();
|
|---|
| 39 | } catch (err) {
|
|---|
| 40 | expect(err).toMatchObject({ code: 'MODULE_NOT_FOUND' });
|
|---|
| 41 | }
|
|---|
| 42 | });
|
|---|
| 43 |
|
|---|
| 44 | it('throws an error', async () => {
|
|---|
| 45 | await foo().catch(error => expect(error).toBeInstanceOf(error));
|
|---|
| 46 | });
|
|---|
| 47 | ```
|
|---|
| 48 |
|
|---|
| 49 | The following patterns are not warnings:
|
|---|
| 50 |
|
|---|
| 51 | ```js
|
|---|
| 52 | it('foo', () => {
|
|---|
| 53 | expect(!value).toBe(false);
|
|---|
| 54 | });
|
|---|
| 55 |
|
|---|
| 56 | function getValue() {
|
|---|
| 57 | if (process.env.FAIL) {
|
|---|
| 58 | return 1;
|
|---|
| 59 | }
|
|---|
| 60 |
|
|---|
| 61 | return 2;
|
|---|
| 62 | }
|
|---|
| 63 |
|
|---|
| 64 | it('foo', () => {
|
|---|
| 65 | expect(getValue()).toBe(2);
|
|---|
| 66 | });
|
|---|
| 67 |
|
|---|
| 68 | it('validates the request', () => {
|
|---|
| 69 | try {
|
|---|
| 70 | processRequest(request);
|
|---|
| 71 | } catch {
|
|---|
| 72 | // ignore errors
|
|---|
| 73 | } finally {
|
|---|
| 74 | expect(validRequest).toHaveBeenCalledWith(request);
|
|---|
| 75 | }
|
|---|
| 76 | });
|
|---|
| 77 |
|
|---|
| 78 | it('throws an error', async () => {
|
|---|
| 79 | await expect(foo).rejects.toThrow(Error);
|
|---|
| 80 | });
|
|---|
| 81 | ```
|
|---|
| 82 |
|
|---|
| 83 | ### How to catch a thrown error for testing without violating this rule
|
|---|
| 84 |
|
|---|
| 85 | A common situation that comes up with this rule is when wanting to test
|
|---|
| 86 | properties on a thrown error, as Jest's `toThrow` matcher only checks the
|
|---|
| 87 | `message` property.
|
|---|
| 88 |
|
|---|
| 89 | Most people write something like this:
|
|---|
| 90 |
|
|---|
| 91 | ```typescript
|
|---|
| 92 | describe('when the http request fails', () => {
|
|---|
| 93 | it('includes the status code in the error', async () => {
|
|---|
| 94 | try {
|
|---|
| 95 | await makeRequest(url);
|
|---|
| 96 | } catch (error) {
|
|---|
| 97 | expect(error).toHaveProperty('statusCode', 404);
|
|---|
| 98 | }
|
|---|
| 99 | });
|
|---|
| 100 | });
|
|---|
| 101 | ```
|
|---|
| 102 |
|
|---|
| 103 | As stated above, the problem with this is that if `makeRequest()` doesn't throw
|
|---|
| 104 | the test will still pass as if the `expect` had been called.
|
|---|
| 105 |
|
|---|
| 106 | While you can use `expect.assertions` & `expect.hasAssertions` for these
|
|---|
| 107 | situations, they only work with `expect`.
|
|---|
| 108 |
|
|---|
| 109 | A better way to handle this situation is to introduce a wrapper to handle the
|
|---|
| 110 | catching, and otherwise returns a specific "no error thrown" error if nothing is
|
|---|
| 111 | thrown by the wrapped function:
|
|---|
| 112 |
|
|---|
| 113 | ```typescript
|
|---|
| 114 | class NoErrorThrownError extends Error {}
|
|---|
| 115 |
|
|---|
| 116 | const getError = async <TError>(call: () => unknown): Promise<TError> => {
|
|---|
| 117 | try {
|
|---|
| 118 | await call();
|
|---|
| 119 |
|
|---|
| 120 | throw new NoErrorThrownError();
|
|---|
| 121 | } catch (error: unknown) {
|
|---|
| 122 | return error as TError;
|
|---|
| 123 | }
|
|---|
| 124 | };
|
|---|
| 125 |
|
|---|
| 126 | describe('when the http request fails', () => {
|
|---|
| 127 | it('includes the status code in the error', async () => {
|
|---|
| 128 | const error = await getError(async () => makeRequest(url));
|
|---|
| 129 |
|
|---|
| 130 | // check that the returned error wasn't that no error was thrown
|
|---|
| 131 | expect(error).not.toBeInstanceOf(NoErrorThrownError);
|
|---|
| 132 | expect(error).toHaveProperty('statusCode', 404);
|
|---|
| 133 | });
|
|---|
| 134 | });
|
|---|
| 135 | ```
|
|---|