Reputation: 24691
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
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:
You have to use jasmine-console-reporter
if you don't:
npm i -D jasmine-console-reporter
test
script inside package.json
to be "test": "tsc && jasmine --reporter=jasmine-console-reporter"
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