如何结合流、Promise与管道编写单元测试?Mocha+Sinon测试方法
Hey there! Let's break down your two questions one by one—both are super common when writing robust Node.js tests, so I'll walk you through practical examples to make it clear.
Streams are event-driven, which can make testing tricky, but wrapping them in Promises (or using Node.js's built-in promise-based stream utilities) lets you use familiar async/await syntax for cleaner tests.
Key Approaches & Examples
Convert Streams to Promises for Easier Async Testing
Node.js v15+ includes stream/promises, which turns stream operations like pipeline into Promise-based functions. This eliminates manual event handling for finish/error events.
Suppose you have a function that reads a file, transforms its content to uppercase, and writes it to an output file via pipes:
const { createReadStream, createWriteStream } = require('fs'); const { pipeline } = require('stream/promises'); const { Transform } = require('stream'); async function processFileStream(inputPath, outputPath) { const upperCaseTransform = new Transform({ transform(chunk, enc, cb) { cb(null, chunk.toString().toUpperCase()); } }); return pipeline( createReadStream(inputPath), upperCaseTransform, createWriteStream(outputPath) ); }
Here's how to test it with Mocha and Chai, using temporary files to avoid polluting your filesystem:
const { expect } = require('chai'); const fs = require('fs/promises'); const path = require('path'); const processFileStream = require('../src/processFileStream'); describe('processFileStream', () => { let tempInput, tempOutput; // Create test files before each test beforeEach(async () => { tempInput = path.join(__dirname, 'temp-input.txt'); await fs.writeFile(tempInput, 'hello world'); tempOutput = path.join(__dirname, 'temp-output.txt'); }); // Clean up after each test afterEach(async () => { await fs.unlink(tempInput).catch(() => {}); await fs.unlink(tempOutput).catch(() => {}); }); it('converts input text to uppercase and writes to output', async () => { await processFileStream(tempInput, tempOutput); const outputContent = await fs.readFile(tempOutput, 'utf8'); expect(outputContent).to.equal('HELLO WORLD'); }); it('rejects with an error if the input file does not exist', async () => { await expect(processFileStream('non-existent-file.txt', tempOutput)) .to.be.rejectedWith('ENOENT: no such file or directory'); }); });
Test with In-Memory Streams for Speed
For faster tests (no disk I/O), use PassThrough streams to simulate input/output without files:
const { PassThrough } = require('stream'); // Modified function to accept streams instead of file paths async function processStream(inputStream, outputStream) { const upperCaseTransform = new Transform({ transform(chunk, enc, cb) { cb(null, chunk.toString().toUpperCase()); } }); return pipeline(inputStream, upperCaseTransform, outputStream); } // Corresponding test it('transforms stream data to uppercase', async () => { const input = new PassThrough(); const output = new PassThrough(); const chunks = []; output.on('data', chunk => chunks.push(chunk)); // Start the pipeline processStream(input, output); // Send test data input.write('hello'); input.end(); // Wait for the stream to finish await new Promise(resolve => output.on('finish', resolve)); expect(Buffer.concat(chunks).toString()).to.equal('HELLO'); });
Mocha handles test organization and execution, while Sinon helps you mock/stub external dependencies so you can isolate your function's logic.
Setup First
Install the required packages:
npm install mocha sinon chai --save-dev
Add a test script to your package.json:
"scripts": { "test": "mocha test/**/*.test.js" }
Test Scenarios & Examples
Testing Synchronous Functions
For simple, pure functions, tests are straightforward:
// src/math.js function multiply(a, b) { return a * b; } // test/math.test.js const { expect } = require('chai'); const multiply = require('../src/math'); describe('multiply function', () => { it('returns the product of two numbers', () => { expect(multiply(3, 4)).to.equal(12); expect(multiply(-2, 5)).to.equal(-10); }); });
Testing Asynchronous Functions (Callbacks)
If your function uses callbacks, use Sinon to stub external dependencies and verify interactions:
// src/fileUtils.js const fs = require('fs'); function getFileSize(path, callback) { fs.stat(path, (err, stats) => { if (err) return callback(err); callback(null, stats.size); }); } // test/fileUtils.test.js const { expect } = require('chai'); const sinon = require('sinon'); const fs = require('fs'); const getFileSize = require('../src/fileUtils'); describe('getFileSize', () => { let statStub; // Replace fs.stat with a stub before each test beforeEach(() => { statStub = sinon.stub(fs, 'stat'); }); // Restore the original function after each test afterEach(() => { statStub.restore(); }); it('returns the file size when the file exists', (done) => { statStub.callsArgWith(1, null, { size: 1024 }); getFileSize('test.txt', (err, size) => { expect(err).to.be.null; expect(size).to.equal(1024); // Verify the stub was called with the right arguments sinon.assert.calledOnceWithExactly(statStub, 'test.txt', sinon.match.func); done(); // Signal Mocha the async test is complete }); }); it('passes an error to the callback if the file is missing', (done) => { const testError = new Error('File not found'); statStub.callsArgWith(1, testError); getFileSize('missing.txt', (err) => { expect(err).to.equal(testError); done(); }); }); });
Testing Asynchronous Functions (Promises)
For Promise-based functions, use async/await with Sinon's stub resolvers/rejecters:
// src/fileUtils.js (Promise version) const fs = require('fs/promises'); async function getFileSize(path) { const stats = await fs.stat(path); return stats.size; } // test/fileUtils.test.js const { expect } = require('chai'); const sinon = require('sinon'); const fs = require('fs/promises'); const getFileSize = require('../src/fileUtils'); describe('getFileSize', () => { let statStub; beforeEach(() => { statStub = sinon.stub(fs, 'stat'); }); afterEach(() => { statStub.restore(); }); it('returns the file size when the file exists', async () => { statStub.resolves({ size: 2048 }); const size = await getFileSize('test.txt'); expect(size).to.equal(2048); sinon.assert.calledOnceWithExactly(statStub, 'test.txt'); }); it('rejects with an error if the file is missing', async () => { const testError = new Error('File not found'); statStub.rejects(testError); await expect(getFileSize('missing.txt')).to.be.rejectedWith(testError); }); });
Using Sinon Spies to Track Function Calls
Spies let you verify if a function was called, how many times, and with what arguments:
// src/notification.js function sendEmail(email, message) { // Real email sending logic } function notifyUser(user, message) { sendEmail(user.email, message); } // test/notification.test.js const { expect } = require('chai'); const sinon = require('sinon'); const { notifyUser, sendEmail } = require('../src/notification'); describe('notifyUser', () => { let sendEmailSpy; beforeEach(() => { sendEmailSpy = sinon.spy(sendEmail); }); it('calls sendEmail with the user\'s email and message', () => { const testUser = { email: 'user@example.com' }; const testMessage = 'Hello there!'; notifyUser(testUser, testMessage); sinon.assert.calledOnceWithExactly(sendEmailSpy, 'user@example.com', 'Hello there!'); }); });
内容的提问来源于stack exchange,提问作者Shamoon

