Green Cloak Guy
Green Cloak Guy

Reputation: 24691

Mocked `fs.createFileSync` and `fs.unlinkSync` are not getting called

I have a function that does a lot of things, but among them is that it copies a file to a special directory, does something with it (calls something to interact with that file without using the fs module), and then deletes the copied file once finished.

import { copyFileSync, unlinkSync } from 'fs';

myOtherFunction(path: string) {
  ...
}

myIOFunction(somePath: string) {
  var copyPath = resolve('otherDir/file2.csv');
  copyFileSync(somePath, copyPath);
  try {
    myOtherFunction(copyPath);
  } finally {
    unlinkFileSync(copyPath);
  }
}

export myFunction() {
  ...
  myIOFunction(resolve('file1.csv));
}

Since only myFunction() is exported (it's the only thing that should be able to be directly interacted with), I have to unit test myOtherFunction() and myIOFunction() through it. Part of that is copyFileSync and unlinkFileSync.

My test looks something like this:

import * as fs from 'fs';
import myFunction from './myFile';

...

it("tests something involving input/output", () => {
  mockCopyFile = spyOn(fs, 'copyFileSync');
  mockUnlinkFile = spyOn(fs, 'unlinkSync');

  ...

  myFunction();

  expect(mockCopyFile).toHaveBeenCalledWith(resolve('file1.csv'), resolve('otherDir/file2.csv'));
  expect(mockUnlinkFile).toHaveBeenCalled();

  ...
});

The test is failing with the errors that neither mockCopyFile nor mockUnlinkFile is called. The problem is that the corresponding functions are called - I've stepped through the test with a debugger, and they execute without issue. So the problem must be that the spies aren't properly attaching themselves.

I don't know how to get them to be called. I've tried doing import * as fs from 'fs' and fs.copyFileSync()/fs.unlinkFileSync() in the file being tested. I've tried putting the mocks in a beforeAll() function. Neither solution helps. I'm mocking several other, not immediately relevant, method calls in the same test spec, and they're all working exactly as intended; it's only this one that isn't, and I can't figure out why.


My package.json includes the following dependencies:

  "scripts": {
    "test": "tsc && jasmine",
  },
"devDependencies": {
    "@types/jasmine": "^3.5.10",
    "@types/node": "^13.7.7",
    "@types/pg": "^7.14.3",
    "copyfiles": "^2.2.0",
    "jasmine": "^3.5.0",
    "jasmine-core": "^3.5.0",
    "jasmine-ts": "^0.3.0",
    "js-yaml": "^3.13.1",
    "mock-fs": "^4.11.0",
    "morgan": "^1.10.0",
    "nodemon": "^2.0.2",
    "swagger-ui-express": "^4.1.3",
    "ts-node": "^8.7.0",
    "typescript": "^3.8.3"
  },
  "dependencies": {
    "@types/express": "^4.17.3",
    "chokidar": "^3.3.1",
    "cors": "^2.8.5",
    "csv-writer": "^1.6.0",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "murmurhash": "0.0.2",
    "pg": "^7.18.2",
    "pg-format": "^1.0.4",
    "winston": "^3.2.1"
  }

and my jasmine.json looks like so:

{
  "spec_dir": "dist",
  "spec_files": [
    "**/*[sS]pec.js"
  ],
  "helpers": [
    "helpers/**/*.js"
  ],
  "stopSpecOnExpectationFailure": false,
  "random": true
}

And tsconfig:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "typeRoots": [
      "node_modules/@types",
      "node_modules/@types/node"
    ],
  },
  "lib": [
    "es2015"
  ]
}

Upvotes: 1

Views: 833

Answers (1)

Christos Lytras
Christos Lytras

Reputation: 37318

Jasmine spyOn mocking function returns a Spy class object which does not represent any function call, but it has helper methods regarding mocking the function. You have to call expect directly to fs.<function> in order to check if it's called:

import * as fs from 'fs';
import * as path from 'path';
import { myFunction } from '../src/myFunction';

describe('MyFunc', () => {
  it("tests something involving input/output", () => {
    spyOn(fs, 'copyFileSync');
    spyOn(fs, 'unlinkSync');

    myFunction();

    expect(fs.copyFileSync).toHaveBeenCalledWith(
      path.resolve('file1.csv'),
      path.resolve('otherDir/file2.csv')
    );
    expect(fs.unlinkSync).toHaveBeenCalled();
  });
});

You can test a simple repro example using this GitHub repo: https://github.com/clytras/fs-jasminte-ts-mocking

git clone https://github.com/clytras/fs-jasminte-ts-mocking.git
cd fs-jasminte-ts-mockin
npm i
npm run test

UPDATE

It appears that you're having esModuleInterop set to true inside tsconfig.json. That means that when you import * as fs from 'fs' that won't keep a single instance of the fs object.

You can set esModuleInterop to false and have your tests passing with toHaveBeenCalled and toHaveBeenCalledWith, but that may break some other functionality of your project. You can read more about what esModuleInterop does here Understanding esModuleInterop in tsconfig file.

If you don't want to set esModuleInterop to false, then you have to import fs as in ES6 Javascript like this:

import fs from 'fs'; // Use plain default import instead of * as
import path from 'path';
import { myFunction } from '../src/myFunction';

describe('MyFunc', () => {
  it("tests something involving input/output", () => {
    spyOn(fs, 'copyFileSync');
    spyOn(fs, 'unlinkSync');

    myFunction();

    expect(fs.copyFileSync).toHaveBeenCalledWith(
      path.resolve('file1.csv'),
      path.resolve('otherDir/file2.csv')
    );
    expect(fs.unlinkSync).toHaveBeenCalled();
  });
});

I also noticed these things missing from your config files:

  1. You have to use jasmine-console-reporter if you don't:

    • npm i -D jasmine-console-reporter
    • Change test script inside package.json to be "test": "tsc && jasmine --reporter=jasmine-console-reporter"
  2. Inside jasmine.json, add ts-node/register/type-check.js to helpers like:

    {
      ...
      "helpers": [
        "helpers/**/*.js",
        "../node_modules/ts-node/register/type-check.js"
      ],
    }
    

Now your tests should be passing.

Upvotes: 4

Related Questions