如何在 sinon 中模拟文件 I/O?

How to mock file I/O in sinon?

我有一个从目录中解压文件的功能。一切正常

index.js

const unZip = async (zipFilePath, destDir) => {
  await util.promisify(fs.mkdir)(destDir);

  return new Promise((resolve, reject) => {
    fs.createReadStream(zipFilePath)
      .pipe(unzipper.Extract({ path: destDir }))
      .on("close", () => resolve(destDir))
      .on("error", (err) => {
        console.log("Error inside unzip", err);
        reject(err);
      });
  });
};

但是对于单元测试,我使用的是 sinonava,我无法通过测试用例 这是代码

index.test.js

ava.beforeEach(() => {
  // mockFs({
  //   'fakeDir/fakeFile': mockFs.load('test/helpers/file/testFile.txt'),
  //   fakeFileContent: 'content here',
  // });
  sinon.stub(mockFs, 'createReadStream').returns({
    pipe: sinon.stub().returns({
      on: sinon.stub().returns({
        on: sinon.stub().returns(),
      }),
    }),
  });
});

ava.serial('unZip test', async (t) => {
  const unzip = proxyquire('../../../src/helpers/file/unZip', {
    fs: mockFs,
    util: {},
    unzipper: { Extract: () => Buffer.from([8, 6, 7, 5, 3, 0, 9]) },
  });

  const result = await unzip('fakeFileContent', 'fakeFileContent');

  t.is(result, true);
});

它给我这样的错误

  unZip test

  Rejected promise returned by test. Reason:

  Error {
    code: 'EEXIST',
    errno: -17,
    path: 'fakeFileContent',
    syscall: 'mkdir',
    message: 'EEXIST: file already exists, mkdir \'fakeFileContent\'',
  }

您不需要使用 proxyquire 包,使用 sinon.stub(obj, 'method') 存根对象的方法。您可以存根 fs.mkdirunzipper.Extractfs.createReadStream 方法。

你用util.promisifyfs.mkdir转成promise形式调用了,但是underly还是回调被调用,所以需要用.callsFake()方法mock fs.mkdir 的实现,并在测试用例中手动调用回调。

以下示例使用 mocha 作为测试框架,但 ava 也应该没问题。

index.js:

const fs = require('fs');
const util = require('util');
const unzipper = require('unzipper');

const unZip = async (zipFilePath, destDir) => {
  await util.promisify(fs.mkdir)(destDir);

  return new Promise((resolve, reject) => {
    fs.createReadStream(zipFilePath)
      .pipe(unzipper.Extract({ path: destDir }))
      .on('close', () => resolve(destDir))
      .on('error', (err) => {
        console.log('Error inside unzip', err);
        reject(err);
      });
  });
};

module.exports = unZip;

index.test.js:

const unZip = require('./');
const fs = require('fs');
const sinon = require('sinon');
const unzipper = require('unzipper');

describe('69616649', () => {
  afterEach(() => {
    sinon.restore();
  });
  it('should pass', async () => {
    sinon.stub(fs, 'mkdir').callsFake((path, callback) => {
      callback();
    });
    const rs = {
      pipe: sinon.stub().returnsThis(),
      on: sinon.stub().callsFake(function (event, callback) {
        if (event === 'close') {
          callback();
        }
      }),
    };
    sinon.stub(fs, 'createReadStream').returns(rs);
    sinon.stub(unzipper, 'Extract');
    const actual = await unZip('fakeFileContent', 'fakeFileContent');
    sinon.assert.match(actual, 'fakeFileContent');
    sinon.assert.calledWithExactly(fs.mkdir, 'fakeFileContent', sinon.match.func);
    sinon.assert.calledWithExactly(fs.createReadStream, 'fakeFileContent');
    sinon.assert.calledWithExactly(unzipper.Extract, { path: 'fakeFileContent' });
    sinon.assert.calledOnce(rs.pipe);
    sinon.assert.calledWithExactly(rs.on, 'close', sinon.match.func);
  });
});

测试结果:

  69616649
    ✓ should pass


  1 passing (7ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |   81.82 |      100 |      75 |   81.82 |                   
 index.js |   81.82 |      100 |      75 |   81.82 | 13-14             
----------|---------|----------|---------|---------|-------------------

我是 ava 的新手所以可能是错误的

proxyquire('../../../src/helpers/file/unZip')// actual function file

ava('69616649', () => {
  ava.afterEach(() => {
    sinon.restore();
  });
  ava.serial('should pass', async () => {
    sinon.stub(fs, 'mkdir').callsFake((path, callback) => {
      callback();
    });
    const rs = {
      pipe: sinon.stub().returnsThis(),
      on: sinon.stub().callsFake(function (event, callback) {
        if (event === 'close') {
          callback();
        }
      }),
    };
    sinon.stub(fs, 'createReadStream').returns(rs);
    sinon.stub(unzipper, 'Extract');
    const actual = await unZip('fakeFileContent', 'fakeFileContent');
    sinon.assert.match(actual, 'fakeFileContent');
    sinon.assert.calledWithExactly(
      fs.mkdir,
      'fakeFileContent',
      sinon.match.func
    );
    sinon.assert.calledWithExactly(fs.createReadStream, 'fakeFileContent');
    sinon.assert.calledWithExactly(unzipper.Extract, {
      path: 'fakeFileContent',
    });
    sinon.assert.calledOnce(rs.pipe);
    sinon.assert.calledWithExactly(rs.on, 'close', sinon.match.func);
  });
});

试试这个

index.test.js

const mockFs = {
  createReadStream: function () {
    return this;
  },
  mkdir: function (p, cb) {
    cb(null, this);
  },
  pipe: function () {
    return this;
  },
  on: function (param, cb) {
    if (param === 'close') {
      return cb();
    }
    if (param === 'error') {
      return this;
    }
  },
};

ava.serial('unZip success', async (t) => {
  const unzip = proxyquire('../../../src/helpers/file/unZip', {
    fs: mockFs,
    util: {},
    unzipper: { Extract: () => Buffer.from([8, 6, 7, 5, 3, 0, 9]) },
  });
  const mockZipFilePath = '../file/testFile.txt';
  const destinationFilePath = '../file';

  const result = await unzip(mockZipFilePath, destinationFilePath);

  t.is(result, destinationFilePath);
});