stubbing process.exit with jest

30.7k Views Asked by At

I have code that does something like

 function myFunc(condition){
  if(condition){
    process.exit(ERROR_CODE)
  }
 }

How can I test this in Jest? Overwriting exit in process with jest.fn() and returning it back after the test doesn't work, since the process exits

7

There are 7 best solutions below

2
On

You could use jest.spyOn as this will call the original method as well:

const exit = jest.spyOn(process, 'exit');
//run your test
expect(exit).toHaveBeenCalledWith('ERROR_CODE');
0
On

This worked for me in terms of spying on process#exit and not getting type errors about the method signature of the mock implementation that required // @ts-ignore'ing:

const processExit = jest
  .spyOn(process, 'exit')
  .mockImplementation((code?: number) => undefined as never);
0
On

Mocking the implementation of process.exit can sometimes interfere with the control flow the code under test because now the process continues after process.exit call. Here is how to stub process.exit with jest and solve this problem:

const mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(((code) => { 
  throw new Error(`Process.exit(${code})`); // Forces the code to throw instead of exit
}));

describe('My Module', () => {
  beforeEach(() => {
    mockProcessExit.mockClear();
  });

  it('Should exit with 99', () => {
    expect(() => myFunction()).toThrowErrorMatchingSnapshot(); // snapshot: Process.exit(99)
  })
})

     
0
On

I faced a similar problem. Solved it with the code below

const setProperty = (object, property, value) => {
    const originalProperty = Object.getOwnPropertyDescriptor(object, property)
    Object.defineProperty(object, property, { value })
    return originalProperty
}

const mockExit = jest.fn()
setProperty(process, 'exit', mockExit)

expect(mockExit).toHaveBeenCalledWith('ERROR_CODE')
7
On

The other suggestions in this thread would cause errors on my end, where any tests with process.exit would run indefinitely. The following option worked for me on TypeScript, but it should work on JavaScript as well:

const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
myFunc(condition);
expect(mockExit).toHaveBeenCalledWith(ERROR_CODE);

The catch is that simply using spyOn meant that the original process.exit() function was still called, ending the process thread and hanging tests. Using mockImplementation at the end replaces the function body with the provided function (which is empty in my example).

This trick is also useful for tests that print to, say, stdout. For example:

const println = (text: string) => { process.stdout.write(text + '\n'); };
const mockStdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => {});
println('This is a text.');
expect(mockStdout).toHaveBeenCalledWith('This is a text.\n');

This will let you test printed values, and have the added benefit of not messing up CLI console output with random linebreaks.


Just one note: As with any "jest.spyOn" call, regardless of using mock implementation or not, you need to restore it later on in order to avoid weird side-effects of lingering mocks. As such, remember to call the two following functions at the end of the current test case:

mockExit.mockRestore()
mockStdout.mockRestore()
0
On

I had a problem with mocking process.exit before importing my module. So importing before mocking worked.

const { foo } = require("my-module");

const realProcessExit = process.exit;
process.exit = jest.fn(() => { throw "mockExit"; });
afterAll(() => { process.exit = realProcessExit; });

describe("foo", () => {
    it("should exit the program", () => {
        try {
            foo();
        } catch (error) {
            expect(error).toBe("mockExit");
            expect(process.exit).toBeCalledWith(1);
        }
    });
});

(important never to return (throw) in mocked process.exit so foo doesn't continue control flow as if nothing happened)

1
On

For most of the global javascript object, I try to replace with my stub and restore after the test. Following works fine for me to mock process.

  describe('myFunc', () => {
    it('should exit process on condition match', () => {
      const realProcess = process;
      const exitMock = jest.fn();

      // We assign all properties of the "real process" to
      // our "mock" process, otherwise, if "myFunc" relied
      // on any of such properties (i.e `process.env.NODE_ENV`)
      // it would crash with an error like:
      // `TypeError: Cannot read property 'NODE_ENV' of undefined`.
      global.process = { ...realProcess, exit: exitMock };

      myFunc(true);
      expect(exitMock).toHaveBeenCalledWith(ERROR_CODE);
      global.process = realProcess;
    });
  });

This help to avoid running the real process.exit to avoid crashing the unit testing.