使用Jest / Typescript测试fs库函数

时间:2018-09-28 15:09:09

标签: typescript testing mocking jestjs

我正在尝试测试我编写的库函数(它在我的代码中有效),但是无法通过fs的模拟进行测试。我有一系列用于与操作系统配合使用的函数,这些函数包装在函数中,以便应用程序的不同部分可以使用相同的调用。

我尝试遵循this question模拟文件系统,但是它似乎对我不起作用。

下面是演示我的问题基础的简短示例:

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();
}

所以现在我正尝试使用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);
    });
});

我得到一个异常,该文件不存在,但我希望没有调用fs.readFileSync的实际调用,而是使用了jest.fn()模拟。

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

我不确定如何进行此模拟吗?

2 个答案:

答案 0 :(得分:3)

自从我提到功能/ OO /以及对开玩笑的嘲笑的厌恶以来,我觉得我应该在这里填写一些解释。

我不反对jest.mock()或任何模拟库(例如sinon)。 我以前使用过它们,它们确实可以达到目的,并且是有用的工具。 但是我发现自己大部分时候都不需要它们,并且在使用它们时会有一些权衡。

首先让我演示不使用模拟即可实现代码的三种方式。

第一种方法是实用的,使用context作为第一个参数:

// 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());
    });
});

第二种方法是使用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());
    });
});

第三种方式使用了修改后的功能样式,该样式需要TypeScript 3.1(从技术上讲,您可以在3.1之前完成,但是涉及名称空间黑客的做法却有些笨拙):

// 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());
    });
});

前两种方式提供了更大的灵活性和隔离性,因为每个调用/实例都有自己的依赖关系引用。 这意味着一个测试的“模拟”将不会影响另一个测试。

第三种方法不会阻止这种情况的发生,但是具有不更改原始功能签名的好处。

所有这些的最底层是依赖性管理。 在大多数情况下,程序或代码难以维护,使用或测试是因为它没有为调用上下文提供控制其被调用者依赖性的方法。

依靠模拟库(尤其是像jest.mock()这样强大的模拟系统)很容易养成忽略这一重要方面的习惯。

我建议所有人都去看看的一篇不错的文章是Bob叔叔的Clean Architecture:https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

答案 1 :(得分:0)

虽然unional的评论帮助我指出了正确的方向,但是fs的导入在我的代码中white lap 中完成了。这似乎是问题所在。将此处的导入更改为import * as fs from 'fs'即可解决问题。

因此,代码变为:

import fs from 'fs'

和测试文件:

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();
}