Search code examples
javascriptnode.jsecmascript-6scriptingjestjs

How do I test a Node.JS Script that is not a module and runs as soon as it's called?


I like to use Javascript as a replacement for bash scripts.

Assuming a contrived script called start.js that is run using node start.js:

const shelljs = require("shelljs")

if (!shelljs.which("serve")) {
  shelljs.echo("'serve' is missing, please run 'npm ci'")
  process.exit(1)
}

shelljs.exec("serve -s build -l 3000")

How do I test that:

  1. If serve isn't available then serve -s build -l 3000 is never called and program exits with code 1.
  2. If serve is available then serve -s build -l 3000 is called.

I don't mind mocking "shelljs", process.exit or anything else.

My main issue is figuring out how to require or import such a functionless and moduleless file in a test suite and get it to run once on each separate test with the mocks present without actually turning this into a CommonJS/ES6 module.


Solution

  • Just mock shelljs module, and spy process.exit function.

    describe("start.js", () => {
      let shelljs
      let exitSpy
    
      beforeEach(() => {
        jest.mock("shelljs", () => {
          return {
            exec: jest.fn(),
            which: jest.fn(),
            echo: jest.fn(),
          }
        })
        shelljs = require("shelljs")
        exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {})
      });
    
      afterEach(() => {
        jest.resetModules()
        jest.resetAllMocks()
      })
    
      it("should execute process.exit with code is 1 when 'serve' is not existed", () => {
        shelljs.which.mockReturnValue(false)
    
        require("./start")
    
        expect(shelljs.which).toHaveBeenCalledWith("serve");
        expect(shelljs.echo).toHaveBeenCalledWith("'serve' is missing, please run 'npm ci'")
        expect(exitSpy).toHaveBeenCalledWith(1)
        // expect(shelljs.exec).toHaveBeenCalled() // can not check like that, exitSpy will not "break" your code, it will be work well if you use if/else syntax
      });
    
      it("should execute serve when 'serve' is existed", () => {
        shelljs.which.mockReturnValue(true)
    
        require("./start")
    
        expect(shelljs.which).toHaveBeenCalledWith("serve");
        expect(shelljs.echo).not.toHaveBeenCalled()
        expect(exitSpy).not.toHaveBeenCalled()
        expect(shelljs.exec).toHaveBeenCalledWith("serve -s build -l 3000")
      });
    })
    
    

    Another way to make sure the production code with be break when process.exit is called. Mock exit function throw an Error, then expect shelljs.exec will not be called

    describe("start.js", () => {
      let shelljs
      let exitSpy
    
      beforeEach(() => {
        jest.mock("shelljs", () => {
          return {
            exec: jest.fn(),
            which: jest.fn(),
            echo: jest.fn(),
          }
        })
        shelljs = require("shelljs")
        
        exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {
          throw new Error("Mock");
        })
      });
    
      afterEach(() => {
        jest.resetModules()
        jest.resetAllMocks()
      })
    
      it("should execute process.exit with code is 1 when 'serve' is not existed", () => {
        shelljs.which.mockReturnValue(false)
    
        expect.assertions(5)
        try {
          require("./start")
        } catch (error) {
          expect(error.message).toEqual("Mock")
        }
    
        expect(shelljs.which).toHaveBeenCalledWith("serve");
        expect(shelljs.echo).toHaveBeenCalledWith("'serve' is missing, please run 'npm ci'")
        expect(exitSpy).toHaveBeenCalledWith(1)
        expect(shelljs.exec).not.toHaveBeenCalled()
      });
    
      it("should execute serve when 'serve' is existed", () => {
        shelljs.which.mockReturnValue(true)
    
        require("./start")
    
        expect(shelljs.which).toHaveBeenCalledWith("serve");
        expect(shelljs.echo).not.toHaveBeenCalled()
        expect(exitSpy).not.toHaveBeenCalled()
        expect(shelljs.exec).toHaveBeenCalledWith("serve -s build -l 3000")
      });
    })