使用Jest/Typescript测试fs库函数

使用Jest/Typescript测试fs库函数,typescript,testing,mocking,jestjs,Typescript,Testing,Mocking,Jestjs,我正在尝试测试一个我已经编写的库函数(它在我的代码中工作),但无法使用fs的mock进行测试。我有一系列用于操作系统的函数包装在函数中,因此应用程序的不同部分可以使用相同的调用 我试着模仿文件系统,但它似乎对我不起作用 下面是一个简短的示例,演示我的问题的基本原理: import * as fs from 'fs'; export function ReadFileContentsSync(PathAndFileName:string):string { if (PathAndFileN

我正在尝试测试一个我已经编写的库函数(它在我的代码中工作),但无法使用fs的mock进行测试。我有一系列用于操作系统的函数包装在函数中,因此应用程序的不同部分可以使用相同的调用

我试着模仿文件系统,但它似乎对我不起作用

下面是一个简短的示例,演示我的问题的基本原理:

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()mock

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

我不知道如何进行此模拟?

虽然unional的评论帮助我指出了正确的方向,但fs的导入是在我的代码中完成的,作为“fs”中的fs导入的。这似乎就是问题所在。将此处的导入更改为从“fs”导入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();
}
和测试文件:

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

既然我提到了functional/OO/和不喜欢jest mock,我觉得我应该在这里补充一些解释

我不反对
jest.mock()
或任何模拟库(例如
sinon
)。 我以前使用过它们,它们绝对符合它们的目的,是一个有用的工具。 但我发现我自己在大多数情况下并不需要它们,而且在使用它们时有一些折衷

让我首先演示三种不使用mock实现代码的方法

第一种方法是功能性的,使用
上下文作为第一个参数:

// 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之前这样做,但涉及到名称空间hack则有点笨拙):

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

第三种方法并不能阻止这种情况的发生,但它的好处是不改变原始函数的签名

所有这些的基础是依赖关系管理。 大多数情况下,程序或代码很难维护、使用或测试是因为它没有为调用上下文提供控制其被调用方依赖性的方法

依赖mocking库(特别是像
jest.mock()
这样强大的mocking系统)很容易养成忽略这一重要方面的习惯


我建议大家阅读的一篇好文章是Bob叔叔的干净体系结构:

尝试在
tsconfig
中启用
esModuleInterop
,使用
从“fs”导入fs
并再次尝试。
import*as fs…
正在进行复制,因此您的直接模拟无法工作。您可以在
jest
中使用lib mock进行模拟,而不是模拟函数。另一方面,考虑函数编程模式/OO来管理您的依赖关系。@ UNIONE,我尝试了启用ESMeMeNeopp的建议,然后从“FS”<代码>导入“代码>导入FS,但是模拟没有发生,因为模拟数据没有返回,而是试图访问显然不存在的文件出错。”code>enoint:没有这样的文件或目录,请打开测试路径
,然后您必须使用jest旁路模拟。就我个人而言,我不是它的朋友。我宁愿遵循函数式编程惯例。我喜欢答案,但有一些注意事项/问题:#1
export function ReadFileContentsSync({fs}={fs})…
这里的第一个参数意味着我们所有的调用代码也必须导入fs,因为它需要传递给库函数。如果我们更改为不同于fs的处理程序,这将涉及很多更改。对于#2,我认为这是更好的选择,除了fs是公共的,并且可以在调用libra时被覆盖我们尝试使用与期望不同的东西(测试所需),因为我们只尝试使用1个库,但代码审查应该会发现问题。对于#1,是的,您的调用代码需要传递它。本质上,这些“外部依赖”应该由“主边界”管理,即应用程序。对于#2,不确定你的意思。你能详细说明一下吗?总的来说,你说的是真的。它是公开的。我的论点是“JavaScript中的所有属性”已经公开了。IMO有时我们过于重视封装。我们应该知道什么可以做,什么不可以做,但没有必要将其限制在一个使我们的工作很难/不可能有效进行的点上。关于“主要边界”,这是一个好的DI解决方案应该可用的原因之一,甚至可以在IMO语言中使用。携带代码所需的所有依赖项是一件很麻烦的事情,而且很容易违反基本设计原则(低级别的细节更改会导致高级别的策略更改)。问题是学会有效和正确地使用DI。这可以解决一系列问题。我们的初级、中级和高级开发人员组合越多。越是初级,越有可能更改公开的内容,他们就越有可能在没有意识到影响的情况下更改。代码审查可以帮助跟踪这一点,并教育青少年rs(和中间产品),但我们可能已经提交了导致问题的代码。但是,我认为带有依赖项注入的类是最好的方法,我将尝试更改我们的库。您所说的“wrap”是什么意思?您的意思是如何存根它?
fs={accessSync(){throw…}
fs={accessSync(){}
// 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());
    });
});