Jest test fails when trying to test an asynchronous function throws

19,986

In your first test:

return getKmlFilename(createEmptyFolder())
  .catch(e => expect(e).toMatch('No valid KML file in'));

It would not complain if the Promise resolves.

In the second test

try {
  const result = await getKmlFilename(createEmptyFolder());
} catch (e) {
  ...
}

It also would not complain if the Promise resolves as it would not get into the catch block.

To test Promises, ask yourself these questions:

  1. Should the promise succeed (resolve) or fail (reject)?
  2. Is your result or rejection value an Error or a regular object?

In jest, you should be able to do this:

  1. Resolve to regular object: expect(yourThing()).resolves.toMatchSnapshot()
  2. Resolve to error (never seen that): expect(yourThing()).resolves.toThrow(/something/)
  3. Rejects to an error: expect(yourThing()).rejects.toThrow(/something/)
  4. Rejects to a regular object (are you sure you want this?): expect(yourThing()).rejects.toMatchSnapshot()

Be aware that an async function always returns a value (a Promise object), so the "usual" expect(() => yourThing()).toThrow() will not work. You need to wait for the result of the Promise first (by using resolves or rejects) and then testing it.

Share:
19,986

Related videos on Youtube

FLC
Author by

FLC

Programmer · Gardener · Builder · Handyman · Parent

Updated on September 28, 2022

Comments

  • FLC
    FLC 2 months

    I have an asynchronous function and I want to test for both: success and failure. On success the function returns a string, on failure it throws. I'm failing miserably at testing failure. Here's my code:

    • getKmlFileName.test.js

    I have disabled by commenting the code that fails and added the results in comments

        'use strict';
    
        const path = require('path');
        const fs = require('fs');
    
        const getKmlFilename = require('./getKmlFileName.js');
    
        const createGoodFolder = () => {
          const folderPath = fs.mkdtempSync('/tmp/test-getKmlFilename-');
          const fileDescriptor = fs.openSync(path.join(folderPath, 'doc.kml'), 'w');
          fs.closeSync(fileDescriptor);
          return folderPath;
        };
    
        const createEmptyFolder = () => fs.mkdtempSync('/tmp/test-getKmlFilename-');
    
        describe('/app/lib/getKmlFilename', () => {
          // Success tests
          test('Should return a KML filename', async () => {
            const result = await getKmlFilename(createGoodFolder());
            expect(result).toMatch(/\.kml$/);
          });
    
          // Failure tests
          test('Should throw if no KML files in folder', () => {
            // Expected one assertion to be called but received zero assertion calls.
            // expect.assertions(1);
    
            // expect(function).toThrow(undefined)
            // Received value must be a function, but instead "object" was found
            //return getKmlFilename(createEmptyFolder())
            // .catch(e => expect(e).toThrow());
    
            // expect(string)[.not].toMatch(expected)
            // string value must be a string.
            // Received:
            // object:
            // [Error: No valid KML file in /tmp/test-getKmlFilename-j2XxQ4]
    
            return getKmlFilename(createEmptyFolder())
              .catch(e => expect(e).toMatch('No valid KML file in'));
          });
    
          test('Should throw if no KML files in folder - try/catch version',
            async () => {
            // Expected one assertion to be called but received zero assertion calls.
            // expect.assertions(1);
    
            try {
              const result = await getKmlFilename(createEmptyFolder());
            } catch (e) {
              // Received value must be a function, but instead "object" was found
              // expect(e).toThrow();
    
              // expect(string)[.not].toMatch(expected)
              // string value must be a string.
              // Received:
              // object:
              // [Error: No valid KML file in /tmp/test-getKmlFilename-3JOUAX]
              expect(e).toMatch('No valid KML file in');
            }
          });
    
        });
    

    As you can see, nothing works. I believe my tests are almost an exact copy of Promises example for the first failure test and Async/Await example for the last one, however none works.

    I believe the difference with the examples from Jest documentation is that they show how to test a function throws and how to test a Promise that rejects. But my promise rejects by throwing.

    Checking the function in the node console I get this log:

    // import function
    > getKml = require('./getKmlFileName.js')
    [AsyncFunction: getKmlFilename]
    // trying it with a proper folder shows we get a Promise
    > getKml('/tmp/con')
    Promise {
      <pending>,
      domain: 
       Domain {
         domain: null,
         _events: { error: [Function: debugDomainError] },
         _eventsCount: 1,
         _maxListeners: undefined,
         members: [] } }
    // trying it with a failing folder shows it's a rejected promise which throws
    > getKml('/tmp/sin')
    Promise {
      <pending>,
      domain: 
       Domain {
         domain: null,
         _events: { error: [Function: debugDomainError] },
         _eventsCount: 1,
         _maxListeners: undefined,
         members: [] } }
    > (node:10711) UnhandledPromiseRejectionWarning: Error: No valid KML file in /tmp/sin
        at getKmlFilename (/home/flc/soft/learning/2018.06.08,jest/getKmlFileName.js:14:11)
        at <anonymous>
        at process._tickDomainCallback (internal/process/next_tick.js:228:7)
    (node:10711) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
    (node:10711) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
    

    As you can see from the inlined comments, the function is doing what it should, however I don't know how to test this in Jest. Any help would be very much appreciated.

    I case the code here looks too convoluted, I prepared a repository which contains my misfortunes learning Jest

    UPDATE 2018.06.12:

    Somehow my message got scrambled and lost the first part which was the actual code I'm trying to test, my apologies for that, here it is:

    • getKmlFileName.js

      'use strict';
      const globby = require('globby');
      const path = require('path');
      
      const getKmlFilename = async (workDir) => {
        const pattern = path.join(workDir, '**/*.kml');
        const files = await globby(pattern);
        if (files && files.length > 0) {
          // Return first KML found, if there are others (improbable), ignore them
          return path.basename(files[0]);
        } else {
          throw new Error(`No valid KML file in ${workDir}`);
        }
      };
      
      module.exports = getKmlFilename;
      
  • FLC
    FLC over 4 years
    The failing tests are the ones that test for failure, the succeding test works fine, is the first one. To answer your points: 1) Both, first test tests success, the other two are two different ways I found in the docs to tests async code. 2) Success result is a string, rejection result is an Error as you can see in my update. My doubt is: will Javascript async take care of wrapping the throw in a reject call? I thought it does. Finally, I'm trying to test for failure, so I don't expect promise to resolve successfully in my later tests. Why expect.assertions(1) doesn't work?
  • FLC
    FLC over 4 years
    Finally got a chance to continue with this, the third way was what I needed to test a promise that throws. Thank you.
  • Árpád Magosányi
    Árpád Magosányi over 2 years
    I guess all of us came for the 3rd way. Could you make that part of your excellent answer more pronounced?
  • Marc
    Marc almost 2 years
    It doesn't look like jest respects parameters here rejects.toThrow(.....). No matter what I pass it accepts it and passes the test. Also if the promise resolves then expect(myPromise()).rejects.toThrow() will still pass the test but complain of UnhandledPromiseRejectionWarning: Error: expect(received).rejects.toThrow() Received promise resolved instead of rejected
  • Marc
    Marc almost 2 years
    Please change your 3rd example to await expect(yourThing()).rejects.toThrow(/yourErrorMessage/)
  • Narigo
    Narigo almost 2 years
    Would it be clear enough if I changed number 3 to expect(yourThing()).rejects.toThrow(/your error message as regular expression match/) ?
  • Mike 'Pomax' Kamermans
    Mike 'Pomax' Kamermans about 1 year
    But why do we need the .resolves. and .rejects. in there? Because the whole point of await is that it wraps a Promise such that it either yields data like any sync function, or throws an error.
  • Narigo
    Narigo about 1 year
    If you await the promise and use expect on the result, that should work as you said - as the thrown error should be caught by the async context. If you're testing a rejected promise, you need the expectation in the catch block but you also need to make sure the promise doesn't accidentally resolve. I guess that's why .rejects. exists then