基于whatsapp-web.js的WhatsApp Bot测试失败,疑与Mock相关
WhatsApp Bot单元测试失败排查求助
我用whatsapp-web.js开发了一款WhatsApp Bot,生产环境运行正常,但执行单元测试时多个用例失败,怀疑问题出在Mock配置上,但无法准确定位。以下是相关代码和错误输出:
WhatsAppBot类实现代码
import qrcode from 'qrcode'; import { Client, LocalAuth, Message } from 'whatsapp-web.js'; import fs from 'fs'; import { createOrUpdateUser, getUserByWhatsAppId, getFaqAnswer, logQuery, } from '../services/dbServices'; import { queryGenerativeAI } from '../services/germini'; const QR_FILE_PATH = './qr-code.json'; const QR_IMAGE_PATH = './qr-code.png'; export class WhatsAppBot { public client: Client; private qrCodeGenerated: boolean; private userSessions: Map<string, string>; constructor() { this.client = new Client({ authStrategy: new LocalAuth(), }); this.qrCodeGenerated = false; this.userSessions = new Map(); this.setupEventHandlers(); } private setupEventHandlers() { this.client.on('qr', async (qrCode: string) => { if (!this.qrCodeGenerated) { try { console.log('Scan this QR code with WhatsApp:'); fs.writeFileSync(QR_FILE_PATH, JSON.stringify({ qrCode }), 'utf-8'); await qrcode.toFile(QR_IMAGE_PATH, qrCode, { errorCorrectionLevel: 'M', scale: 8, margin: 4, color: { dark: '#000000', light: '#ffffff' }, }); const terminalQR = await qrcode.toString(qrCode, { type: 'terminal', small: true }); console.log(terminalQR); console.log(`QR code saved to ${QR_FILE_PATH} and ${QR_IMAGE_PATH}`); this.qrCodeGenerated = true; } catch (error) { console.error('Error generating QR code:', error); } } }); this.client.on('authenticated', () => { console.log('Authenticated successfully!'); }); this.client.on('auth_failure', (msg: string) => { console.error('Authentication failed:', msg); }); this.client.on('ready', () => { console.log('WhatsApp bot is ready!'); }); this.client.on('disconnected', (reason: string) => { console.log('Client was logged out:', reason); }); this.client.on('message', async (message: Message) => { const userId = message.from; if(!userId) return console.log(`Invalid userId: ${userId}`); console.log(`Message received from ${userId}:`, message.body); if (!this.userSessions.has(userId)) { this.userSessions.set(userId, ''); await message.reply("Hi! I'm Chaty, your support assistant. What's your name?"); return; } const userName = this.userSessions.get(userId); if (!userName) { this.userSessions.set(userId, message.body); await createOrUpdateUser(userId, message.body); await message.reply(`Nice to meet you, ${message.body}! How can I assist you today? you can type 'help' to see a list of commands`); return; } const query = message.body.toLowerCase(); if (query === 'help') { await message.reply("type: 'exit' to end the session and 'reset' to reset the session"); return; } if (query === 'exit') { this.userSessions.delete(userId); await message.reply('Your session has been ended. Have a great day!'); return; } if (query === 'reset') { this.userSessions.set(userId, ''); await message.reply("Let's start over. What's your name?"); return; } const faqAnswer = await getFaqAnswer(query); let aiResponse = 'AI generated response'; if (faqAnswer) { await message.reply(faqAnswer); } else { aiResponse = process.env.NODE_ENV === 'test' ? 'AI generated response' : await queryGenerativeAI(query); await message.reply(aiResponse); } const user = await getUserByWhatsAppId(userId); if (user) { await logQuery(user.id, query, faqAnswer || aiResponse); } }); this.client.on('message_ack', (message: Message, ack: number) => { const status = ['MESSAGE SENT', 'MESSAGE DELIVERED', 'MESSAGE READ', 'MESSAGE PLAYED']; console.log(`Message "${message.body}" acknowledgment status: ${status[ack] || 'UNKNOWN'}`); }); } public initialize() { if (fs.existsSync(QR_FILE_PATH)) { console.log(`QR code already saved at ${QR_FILE_PATH}. Please use it for reconnection.`); } else { console.log('No saved QR code found. Generating a new QR code...'); } this.client.initialize(); } public async stop() { try { await this.client.destroy(); console.log('WhatsApp bot stopped'); } catch (err) { console.error('Error stopping WhatsApp bot:', err); } } }
测试用例代码
import { expect } from 'chai'; import * as sinon from 'sinon'; import * as proxyquire from 'proxyquire'; import { Message } from 'whatsapp-web.js'; proxyquire.noCallThru(); describe('WhatsAppBot', () => { let bot: any; let sandbox: sinon.SinonSandbox; let mockClient: { on: sinon.SinonStub; initialize: sinon.SinonStub; destroy: sinon.SinonStub; sendMessage: sinon.SinonStub; }; before(() => { sandbox = sinon.createSandbox(); // Mock Client methods mockClient = { on: sandbox.stub(), initialize: sandbox.stub(), destroy: sandbox.stub(), sendMessage: sandbox.stub(), }; const mockLocalAuth = sandbox.stub(); const WhatsAppBot = proxyquire.load('../src/botHandler/whatsappBot', { 'whatsapp-web.js': { Client: sandbox.stub().returns(mockClient), LocalAuth: mockLocalAuth, }, qrcode: { toFile: sandbox.stub().resolves(), toString: sandbox.stub().resolves(), }, fs: { writeFileSync: sandbox.stub(), existsSync: sandbox.stub().returns(false), }, '../services/dbServices': { createOrUpdateUser: sandbox.stub().resolves(), getUserByWhatsAppId: sandbox.stub().resolves(), getFaqAnswer: sandbox.stub().resolves('The weather today is sunny.'), logQuery: sandbox.stub().resolves(), }, '../services/germini': { queryGenerativeAI: sandbox.stub().resolves('AI generated response'), }, }).WhatsAppBot; bot = new WhatsAppBot(); }); it('should generate QR code on initialization', async () => { const qrCode = 'qrCodeString'; mockClient.on.withArgs('qr').callsFake((event, handler) => { if (event === 'qr') handler(qrCode); }); await bot.initialize(); expect(mockClient.on.calledWith('qr')).to.be.true; expect(mockClient.initialize.called).to.be.true; }); it('should handle new user message', async () => { const userId = '123'; const userName = 'John Doe'; const message: Message = { from: userId, body: userName } as Message; mockClient.on.withArgs('message').callsFake((event, handler) => { console.log('Event triggered:', event); if (event === 'message') handler(message); }); await bot.initialize(); console.log('User sessions:', bot.userSessions); console.log('SendMessage calls:', mockClient.sendMessage.args); expect(bot.userSessions.get(userId)).to.equal(userName); expect(mockClient.sendMessage.calledWith(userId, sinon.match.string)).to.be.true; }); it('should handle existing user query with FAQ answer', async () => { const userId = '123'; const query = 'What is the weather today?'; const message: Message = { from: userId, body: query } as Message; bot.userSessions.set(userId, 'John Doe'); mockClient.on.withArgs('message').callsFake((event, handler) => { if (event === 'message') handler(message); }); await bot.initialize(); expect(mockClient.sendMessage.calledWith(userId, 'The weather today is sunny.')).to.be.true; }); it('should handle existing user query with AI response', async () => { // Arrange const userId = '123'; const query = 'Tell me a joke.'; const aiResponse = 'Why don’t scientists trust atoms? Because they make up everything!'; const message: Message = { from: userId, body: query } as Message; bot.userSessions.set(userId, 'John Doe'); sandbox.stub(bot, 'getFaqAnswer').resolves(null); sandbox.stub(bot, 'queryGenerativeAI').resolves(aiResponse); mockClient.on.withArgs('message').callsFake((event, handler) => { if (event === 'message') handler(message); }); await bot.initialize(); expect(mockClient.sendMessage.calledWith(userId, aiResponse)).to.be.true; }); it('should handle "exit" command', async () => { const userId = '123'; const message: Message = { from: userId, body: 'exit' } as Message; bot.userSessions.set(userId, 'John Doe'); mockClient.on.withArgs('message').callsFake((event, handler) => { if (event === 'message') handler(message); }); await bot.initialize(); expect(mockClient.sendMessage.calledWith(userId, 'Your session has been ended. Have a great day!')).to.be.true; expect(bot.userSessions.has(userId)).to.be.false; }); it('should handle "reset" command', async () => { const userId = '123'; const message: Message = { from: userId, body: 'reset' } as Message; bot.userSessions.set(userId, 'John Doe'); mockClient.on.withArgs('message').callsFake((event, handler) => { if (event === 'message') handler(message); }); await bot.initialize(); expect(mockClient.sendMessage.calledWith(userId, "Let's start over. What's your name?")).to.be.true; expect(bot.userSessions.get(userId)).to.equal(''); }); });
测试失败控制台输出
WhatsAppBot No saved QR code found. Generating a new QR code... ✔ should generate QR code on initialization No saved QR code found. Generating a new QR code... User sessions: Map(0) {} SendMessage calls: [] 1) should handle new user message No saved QR code found. Generating a new QR code... 2) should handle existing user query with FAQ answer 3) should handle existing user query with AI response No saved QR code found. Generating a new QR code... 4) should handle "exit" command No saved QR code found. Generating a new QR code... 5) should handle "reset" command 5 passing (255ms) 5 failing 1) WhatsAppBot should handle new user message: AssertionError: expected undefined to equal 'John Doe' at Context.<anonymous> (test/whatsappBot.test.ts:91:45) at processTicksAndRejections (node:internal/process/task_queues:95:5) 2) WhatsAppBot should handle existing user query with FAQ answer: AssertionError: expected false to be true + expected - actual -false +true at Context.<anonymous> (test/whatsappBot.test.ts:111:91) at processTicksAndRejections (node:internal/process/task_queues:95:5) 3) WhatsAppBot should handle existing user query with AI response: TypeError: Cannot stub non-existent property getFaqAnswer at Function.stub (node_modules/sinon/lib/sinon/stub.js:82:15) at Sandbox.stub (node_modules/sinon/lib/sinon/sandbox.js:454:39) at Context.<anonymous> (test/whatsappBot.test.ts:122:13) at processImmediate (node:internal/timers:483:21) 4) WhatsAppBot should handle "exit" command: AssertionError: expected false to be true + expected - actual -false +true at Context.<anonymous> (test/whatsappBot.test.ts:150:110) at processTicksAndRejections (node:internal/process/task_queues:95:5) 5) WhatsAppBot should handle "reset" command: AssertionError: expected false to be true + expected - actual -false +true at Context.<anonymous> (test/whatsappBot.test.ts:168:99) at processTicksAndRejections (node:internal/process/task_queues:95:5)
问题排查与修复方案
1. 核心问题:事件处理器未正确触发
Bot实例创建时已调用setupEventHandlers()绑定了message事件,后续测试中用mockClient.on.withArgs('message').callsFake(...)无法覆盖原有绑定(Sinon的stub是追加逻辑而非替换),导致测试时实际未触发消息处理逻辑。
修复:
改用beforeEach()创建新Bot实例,提取预绑定的message事件处理器,测试时直接调用处理器而非依赖事件触发:
describe('WhatsAppBot', () => { let bot: any; let sandbox: sinon.SinonSandbox; let mockClient: { on: sinon.SinonStub; initialize: sinon.SinonStub; destroy: sinon.SinonStub; sendMessage: sinon.SinonStub; }; let messageHandler: any; beforeEach(() => { sandbox = sinon.createSandbox(); mockClient = { on: sandbox.stub(), initialize: sandbox.stub(), destroy: sandbox.stub(), sendMessage: sandbox.stub(), }; const WhatsAppBot = proxyquire.load('../src/botHandler/whatsappBot', { 'whatsapp-web.js': { Client: sandbox.stub().returns(mockClient), LocalAuth: sandbox.stub(), }, // 其他mock保持不变 }).WhatsAppBot; bot = new WhatsAppBot(); // 提取message事件的处理器 messageHandler = mockClient.on.withArgs('message').getCall(0).args[1]; }); afterEach(() => { sandbox.restore(); }); // 修改测试用例:直接调用messageHandler it('should handle new user message', async () => { const userId = '123'; const userName = 'John Doe'; // 给message对象添加reply方法,映射到mockClient.sendMessage const message: any = { from: userId, body: userName, reply: async (text: string) => mockClient.sendMessage(userId, text) }; await messageHandler(message); expect(bot.userSessions.get(userId)).to.equal(userName); expect(mockClient.sendMessage.called).to.be.true; }); });
2. 错误:尝试Stub非实例方法
getFaqAnswer和queryGenerativeAI是外部导入的函数,不是WhatsAppBot的实例方法,直接Stubbot.getFaqAnswer会报错。
修复:
通过proxyquire的mock对象或直接Stub导入的模块来修改返回值:
it('should handle existing user query with AI response', async () => { const userId = '123'; const query = 'Tell me a joke.'; const aiResponse = 'Why don’t scientists trust atoms? Because they make up everything!'; const message: any = { from: userId, body: query, reply: async (text: string) => mockClient.sendMessage(userId, text) }; bot.userSessions.set(userId, 'John Doe'); // 修改外部模块的返回值 sandbox.stub(require('../services/dbServices'), 'getFaqAnswer').resolves(null
相关产品推荐
相关产品推荐

