Why is the Promise reject() in my jest.mock going to a then() rather than a catch()?
Asked Answered
Z

2

10

I have two files, getItemInfo.js to make API calls and getItemInfo.test.js which is the respective Jest test file.

On the test file, I am mocking the http calling triggered by node module request-promise.

The question is on the second code block, surrounded by *********'s. Basically why is the reject() error still going to a then() block in the second unit test?

// getItemInfo.js

const rp = require('request-promise');

const getItemInfo = (id) => {
    const root = 'https://jsonplaceholder.typicode.com/posts/';
    const requestOptions = {
        uri: `${root}/${id}`,
        method: 'GET',
        json: true
    }

    return rp(requestOptions)
    .then((result) => {
        return result;
    })
    .catch((err) => {
        return err;
    });
};

module.exports = {
    getItemInfo: getItemInfo
};

And here is my Jest unit test file.

// getItemInfo.test.js
const ItemService = require('./getItemInfo');

jest.mock('request-promise', () => (options) => {
    const id = Number.parseInt(options.uri.substring(options.uri.lastIndexOf('/') + 1));

    return new Promise((resolve, reject) => {
        if (id === 12) {
            return resolve({
                id: id,
                userId: 1,
                title: '',
                body: ''
            });
        } else {
            return reject('something went wrong'); // <-- HERE IS THE REJECT
        }
    })
});

describe('getItemInfo', () => {
    it('can pass', done => {
        const TEST_ID = 12
        ItemService.getItemInfo(TEST_ID).then((result) => {
            console.log('result:',result);
            expect(result.id).toBe(TEST_ID);
            expect(result.userId).toBeDefined();
            expect(result.title).toBeDefined();
            expect(result.body).toBeDefined();
            done();
        });
    });

    it('can fail', (done) => {
        const TEST_ID = 13;
        ItemService.getItemInfo(TEST_ID)
        .catch((err) => {
            // *************
            // This "catch" block never runs
            // even if the jest.mock above Promise.rejects
            // Why is that???
            // *************
            console.log('catch():', err);
            done();
        })
        .then((result) => {
            // this block runs instead.
            // and it returns "then: something went wrong"
            console.log('then():', result);
            done();
        });
    });
});

This is the unit test's output. The command is simply jest. The last line should be run from the catch() statement, not the then():

PASS  ./getItemInfo.test.js
 getItemInfo
   ✓ can pass (9ms)
   ✓ can fail (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.703s, estimated 1s
Ran all test suites.
----------------|----------|----------|----------|----------|----------------|
File            |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
----------------|----------|----------|----------|----------|----------------|
All files       |      100 |      100 |      100 |      100 |                |
getItemInfo.js |      100 |      100 |      100 |      100 |                |
----------------|----------|----------|----------|----------|----------------|
 console.log getItemInfo.test.js:25
   result: { id: 12, userId: 1, title: '', body: '' }

 console.log getItemInfo.test.js:48
   then(): something went wrong

What am I doing wrong?

Zabaglione answered 8/8, 2017 at 6:3 Comment(1)
Because .catch((err) => { return err; }); handles the error! You probably don't want to do that.Leaseback
E
13

Why is the Promise reject() in my jest.mock going to a then() rather than a catch()?

Your .catch() handler is converting a rejected promise into a resolved promise and thus only the outer .then() handler is called.

When you use .catch() like this:

.catch((err) => {
    return err;
});

and do not rethrow the error or return a rejected promise, then the rejection is considered "handled" and the returned promise becomes resolved, not rejected. This is just like using a try/catch. In a catch handler, the exception is handled unless you throw it again.

You can see that in this simple snippet:

new Promise((resolve, reject) => {
    reject(new Error("reject 1"));
}).catch(err => {
    // returning a normal value here (anything other than a rejected promise)
    // causes the promise chain to flip to resolved
    return err;
}).then(val => {
    console.log("Promise is now resolved, not rejected");
}).catch(err => {
    console.log("Don't get here");
});

There's really no reason for either of these:

.then((result) => {
    return result;
})
.catch((err) => {
    return err;
});

You can just remove both of them. The .then() handler is just superfluous code and the .catch() handler eats the rejection and turns it into a resolved promise.

If you want to keep the .catch() handler, but allow the rejection to propagate upwards, then you need to rethrow.

.catch((err) => {
    console.log(err);
    throw err;       // keep promise rejected so reject will propagate upwards
});
Erose answered 8/8, 2017 at 6:8 Comment(0)
I
2

Because you've converted the rejection into a resolution:

.catch((err) => {
    return err;
});

If you want the rejection to propagate out of getItemInfo, remove the catch handler from getItemInfo.

Remember that catch and then both create and return promises, and their handlers are treated the same:

  1. If you return a value from them, the promise then/catch created is fulfilled with that value.

  2. If you return a thenable from them, the promise then/catch created is resolved to that thenable (fulfilled or rejected based on what that thenable does).

  3. If you throw within them, the promise then/catch created is rejected with that error.

(FWIW, I go into promise terminology — "fulfill" vs. "resolve," etc. — in this post on my blog.)

You only need a catch handler if:

  • You're not passing the chain along to something else (which you are in getItemInfo), or

  • You need to transform an error in some way, either by transforming it into a resolution (recovery) or transforming it into a different error. To do the latter you throw or return a promise that is or will be rejected.

Isaac answered 8/8, 2017 at 6:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.