Testing a fs library function with Jest/Typescript

2019-08-01 09:40发布

I am trying to test a library function that I have written (it works in my code) but cannot get the testing with the mock of the fs to work. I have a series of functions for working with the OS wrapped in functions so different parts of the application can use the same calls.

I have tried to follow this question with mocking the file system, but it does not seem to work for me.

A short sample to demonstrate the basics of my issue are below:

import * as fs from 'fs';
export function ReadFileContentsSync(PathAndFileName:string):string {
    if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
        throw new Error('Need a Path and File');
    }
    return fs.readFileSync(PathAndFileName).toString();
}

So now I am trying to test this function using Jest:

import { ReadFileContentsSync } from "./read-file-contents-sync";
const fs = require('fs');

describe('Return Mock data to test the function', () => {
    it('should return the test data', () => {
        const TestData:string = 'This is sample Test Data';

// Trying to mock the reading of the file to simply use TestData
        fs.readFileSync = jest.fn();                
        fs.readFileSync.mockReturnValue(TestData);

// Does not need to exist due to mock above     
        const ReadData = ReadFileContentsSync('test-path');
        expect(fs.readFileSync).toHaveBeenCalled();
        expect(ReadData).toBe(TestData);
    });
});

I get an exception that the file does not exist, but I would have expected the actual call to fs.readFileSync to not have been called, but the jest.fn() mock to have been used.

ENOENT: no such file or directory, open 'test-path'

I am not sure how to do this mock?

2条回答
来,给爷笑一个
2楼-- · 2019-08-01 10:24

While unional's comment helped to point me in the right direction, the import for the fs, was done in my code as import * as fs from 'fs'. This seemed to be the problem. Changing the import here to simply be import fs from 'fs' and that solved the problem.

Therefore, the code becomes:

import fs from 'fs';
export function ReadFileContentsSync(PathAndFileName:string):string {
    if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
        throw new Error('Need a Path and File');
    }
    return fs.readFileSync(PathAndFileName).toString();
}

And the test file:

jest.mock('fs');
import { ReadFileContentsSync } from "./read-file-contents-sync";

import fs from 'fs';

describe('Return Mock data to test the function', () => {
    it('should return the test data', () => {
        const TestData:Buffer = new Buffer('This is sample Test Data');

// Trying to mock the reading of the file to simply use TestData
        fs.readFileSync = jest.fn();                
        fs.readFileSync.mockReturnValue(TestData);

// Does not need to exist due to mock above     
        const ReadData = ReadFileContentsSync('test-path');
        expect(fs.readFileSync).toHaveBeenCalled();
        expect(ReadData).toBe(TestData.toString());
    });
});
查看更多
对你真心纯属浪费
3楼-- · 2019-08-01 10:31

Since I mentioned about functional / OO / and the dislike of jest mock, I feel like I should fill in some explanation here.

I'm not against jest.mock() or any mocking library (such as sinon). I have used them before, and they definitely serve their purpose and is a useful tool. But I find myself do not need them for the most part, and there is some tradeoff when using them.

Let me first demonstrate three ways that the code can be implemented without the use of mock.

The first way is functional, using a context as the first argument:

// read-file-contents-sync.ts
import fs from 'fs';
export function ReadFileContentsSync({ fs } = { fs }, PathAndFileName: string): string {
    if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
        throw new Error('Need a Path and File');
    }
    return fs.readFileSync(PathAndFileName).toString();
}

// read-file-contents-sync.spec.ts
import { ReadFileContentsSync } from "./read-file-contents-sync";

describe('Return Mock data to test the function', () => {
    it('should return the test data', () => {
        const TestData:Buffer = new Buffer('This is sample Test Data');

        // Trying to mock the reading of the file to simply use TestData
        const fs = {
            readFileSync: () => TestData
        }

        // Does not need to exist due to mock above     
        const ReadData = ReadFileContentsSync({ fs }, 'test-path');
        expect(ReadData).toBe(TestData.toString());
    });
});

The second way is to use OO:

// read-file-contents-sync.ts
import fs from 'fs';
export class FileReader {
    fs = fs
    ReadFileContentsSync(PathAndFileName: string) {
        if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
            throw new Error('Need a Path and File');
        }
        return this.fs.readFileSync(PathAndFileName).toString();
    }
}

// read-file-contents-sync.spec.ts
import { FileReader } from "./read-file-contents-sync";

describe('Return Mock data to test the function', () => {
    it('should return the test data', () => {
        const TestData: Buffer = new Buffer('This is sample Test Data');

        const subject = new FileReader()
        subject.fs = { readFileSync: () => TestData } as any

        // Does not need to exist due to mock above     
        const ReadData = subject.ReadFileContentsSync('test-path');
        expect(ReadData).toBe(TestData.toString());
    });
});

The third way uses a modified functional style, which requires TypeScript 3.1 (technically you can do that prior to 3.1, but it is just a bit more clumsy involving namespace hack):

// read-file-contents-sync.ts
import fs from 'fs';
export function ReadFileContentsSync(PathAndFileName: string): string {
    if (PathAndFileName === undefined || PathAndFileName === null || PathAndFileName.length === 0) {
        throw new Error('Need a Path and File');
    }
    return ReadFileContentsSync.fs.readFileSync(PathAndFileName).toString();
}
ReadFileContentsSync.fs = fs

// read-file-contents-sync.spec.ts
import { ReadFileContentsSync } from "./read-file-contents-sync";

describe('Return Mock data to test the function', () => {
    it('should return the test data', () => {
        const TestData: Buffer = new Buffer('This is sample Test Data');

        // Trying to mock the reading of the file to simply use TestData
        ReadFileContentsSync.fs = {
            readFileSync: () => TestData
        } as any

        // Does not need to exist due to mock above     
        const ReadData = ReadFileContentsSync('test-path');
        expect(ReadData).toBe(TestData.toString());
    });
});

The first two ways provide more flexibility and isolation because each call/instance have their own reference of the dependency. This means there will be no way that the "mock" of one test would affect the other.

The third way does not prevent that from happening but have the benefit of not changing the signature of the original function.

The bottom of all these is dependency management. Most of the time a program or code is hard to maintain, use, or test is because it does not provide a way for the calling context to control the dependency of its callee.

Relying on mocking library (especially a mocking system as powerful as jest.mock()) can easily get to a habit of ignoring this important aspect.

One nice article I would recommend everyone to check out is Uncle Bob's Clean Architecture: https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

查看更多
登录 后发表回答