4

My NodeJS app has a function readFilesJSON() that calls fs.readFile(), which of course invokes a callback with the parameters (err,data). The Jest unit test needs to walk both the error path and the data path.

My solution was to mock the call to fs.readFile() (see below). The mock function simply passes error or data based on test logic. This approach works when there is only one function being tested. The trouble I am seeing occurs when there are multiple functions that call fs.readFile(). Jest runs all the tests concurrently and the asynchronous nature of the functions mean that there is no guaranteed ordering to the calls to fs.readFile(). This non-deterministic behavior wrecks both the error/data logic and the parameter-checking logic using toHaveBeenCalledWith().

Does Jest provide a mechanism for managing independent usage of the mocks?

function readFilesJSON(files,done) {
    let index = 0;
    readNextFile();
    function readNextFile() {
        if( index === files.length ) {
            done();
        }
        else {
            let filename = files[index++];
            fs.readFile( filename, "utf8", (err,data) => {
                if(err) {
                    console.err(`ERROR: unable to read JSON file ${filename}`);
                    setTimeout(readNextFile);
                }
                else {
                    // parse the JSON file here
                    // ...
                    setTimeout(readNextFile);
                }
            });
        }
    }
}

The injected function setup looks like this:

jest.spyOn(fs, 'readFile')
    .mockImplementation(mockFsReadFile)
    .mockName("mockFsReadFile");

function mockFsReadFile(filename,encoding,callback) {
    // implement error/data logic here
}

1 Answer 1

2

You can separate the different scenarios in different describe blocks and call your function after you clear the previous calls on the observed function, not to get false positive results.


import { readFile } from "fs";

import fileParser from "./location/of/your/parser/file";

jest.mock("fs");

// mock the file parser as we want to test only readFilesJSON
jest.mock("./location/of/your/parser/file");

describe("readFilesJSON", () => {
  describe("on successful file read attempt", () => {
    let result;

    beforeAll(() => {
      // clear previous calls
      fileParser.mockClear();
      readFile.mockImplementation((_filename, _encoding, cb) => {
        cb(null, mockData);
      });
      result = readFilesJSON(...args);
    });

    it("should parse the file contents", () => {
      expect(fileParser).toHaveBeenCalledWith(mockData);
    });
  });

  describe("on non-successful file read attempt", () => {
    let result;

    beforeAll(() => {
      // clear previous calls
      fileParser.mockClear();
      readFile.mockImplementation((_filename, _encoding, cb) => {
        cb(new Error("something bad happened"), "");
      });
      result = readFilesJSON(...args);
    });

    it("should parse the file contents", () => {
      expect(fileParser).not.toHaveBeenCalled();
    });
  });
});

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the quick response. That is interesting: each test provides a separate instance of the mock function. Let me ask you this: if Test1 is still executing when Test2 calls fileParser.mockClear(), does Test1 still retain its context for all the fileParser mocks? My naïve reading of the docs left me thinking that all the mock instances would no longer exist after calling mockFn.mockClear(). I will try it out.
@LeeJenkins Test2 will not begin until Test1 ends. The separate test files will run in parallel. here's an example
Thanks! I misunderstood the execution model. My tests -- all in the same file -- were running in parallel, in a broken sort of way. I expected the tests to handle asynchronous execution out-of-the-box. Your answer and comment led me to new questions and I have since learned that you have to setup the test to be asynchronous aware. See jestjs.io/docs/en/asynchronous.html
this was brilliant

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.