使用 Jest/Typescript 测试 fs 库函数

Ste*_*ott 4 testing mocking typescript jestjs

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

我试图通过模拟文件系统来解决这个问题,但它似乎对我不起作用。

演示我的问题的基础知识的简短示例如下:

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();
}
Run Code Online (Sandbox Code Playgroud)

所以现在我正在尝试使用 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);
    });
});
Run Code Online (Sandbox Code Playgroud)

我得到一个异常,该文件不存在,但我预计对 fs.readFileSync 的实际调用没有被调用,但 jest.fn() 模拟已被使用。

ENOENT: no such file or directory, open 'test-path'
Run Code Online (Sandbox Code Playgroud)

我不知道如何做这个模拟?

uni*_*nal 7

既然提到了函数式/OO/以及不喜欢jest mock,我觉得我应该在这里填写一些解释。

我不反对jest.mock()或任何嘲笑库(例如sinon)。我以前使用过它们,它们绝对可以达到目的并且是一个有用的工具。但我发现我自己在大多数情况下不需要它们,并且在使用它们时需要进行一些权衡。

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

第一种方法是函数式的,使用 acontext作为第一个参数:

// 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());
    });
});
Run Code Online (Sandbox Code Playgroud)

第二种方式是使用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());
    });
});
Run Code Online (Sandbox Code Playgroud)

第三种方式使用修改后的函数样式,它需要 TypeScript 3.1(从技术上讲,您可以在 3.1 之前做到这一点,但它涉及命名空间 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());
    });
});
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

我推荐大家看看的一篇好文章是 Uncle Bob 的 Clean Architecture:https : //8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html