node.jsjestjsexecnode-promisify

How to mock promisified child process exec with Jest


I'm trying to write a test for this piece of code:

const util = require('node:util')
const exec = util.promisify(require('node:child_process').exec)

class Command {
  async execute(command) {
    try {
      const { stdout, stderr } = await exec(command)
      return { code: 0, stdout, stderr }
    } catch (error) {
      return {
        code: error.code,
        stdout: error.stdout,
        stderr: error.stderr
      }
    }
  }
}

I've looked at How to mock a promisified and bound function? and How do I use Jest to mock a promisified function? but I cannot figure out how exactly mock exec or whether I should mock promisify.

I tried this one:

const { Command } = require('../src/utils')

jest.mock('node:child_process', () => ({
  exec: jest.fn()
}))

describe('Command', () => {
  it('executes a command', async () => {
    const command = new Command()
    const cmd = 'la -la'
    const expected = {
      code: 0,
      stdout: cmd,
      stderr: ''
    }

    require('node:child_process').exec.mockReturnValueOnce({
      stdout: cmd,
      stderr: ''
    })

    const result = await command.execute(cmd)
    expected(result).toEqual(expected)
  })
})

but I get thrown: "Exceeded timeout of 5000 ms for a test..

What should I mock, and how?


Solution

  • I've looked at comments from @jonrsharpe and other similar answers. Also found this answer which provides some clue how to mock exec. Finally made some progress and it works now.

    The reasons why I failed before were:

    1. I did not get that I needed to call callback function in mock
    2. When I got the above, I was confused on how to actually call it because in docs for exec there are three arguments defined for exec (command, options, callback) but if my code do not define options, I should not define it in mock.
    3. Also I probably don't know how to read docs. Exec docs define three arguments for callback (error, stdout, stderr). I was thinking that I should call the callback like callback(null, 'stdout', 'stderr') or maybe callback({error: null, stdout: 'stdout', stderr: 'stderr'}) but based on other answers it should actually be callback(null, {stdout: 'stdout', stderr: 'stderr'}) for some reason.

    Anyways I finally was able to make it work with this:

    const { Command } = require('../src/utils')
    
    jest.mock('node:child_process', () => ({
      exec: jest
        .fn()
        .mockImplementation((command, callback) =>
          callback(null, { stdout: command, stderr: '' })
        )
    }))
    
    describe('Command', () => {
      it('executes a command', async () => {
        const command = new Command()
        const cmd = 'la -la'
        const expected = {
          code: 0,
          stdout: cmd,
          stderr: ''
        }
        const result = await command.execute(cmd)
        expect(result).toEqual(expected)
      })
    })