Nock模拟GraphQL订阅:实时更新测试实践
【免费下载链接】nock 项目地址: https://gitcode.com/gh_mirrors/noc/nock
你是否在测试GraphQL订阅时遇到过这些问题?第三方服务不稳定导致测试失败、WebSocket连接难以模拟、实时数据流难以复现?本文将展示如何使用Nock(HTTP请求模拟库)解决这些痛点,通过三个步骤实现GraphQL订阅的本地测试:建立持久连接、模拟实时数据流、验证客户端行为。完成后你将获得一套可重复执行的测试方案,彻底摆脱对外部服务的依赖。
为什么需要模拟GraphQL订阅
GraphQL订阅(Subscription)通过WebSocket(Web套接字)实现实时数据推送,是构建实时应用的核心技术。但在测试环境中,它带来了特殊挑战:
- 连接持久性:WebSocket连接保持打开状态,传统的单次请求模拟无法满足需求
- 双向通信:客户端与服务器持续交换数据,需要模拟完整的对话流程
- 时序敏感:实时数据的到达顺序直接影响应用状态,测试需精确控制时间
Nock作为Node.js生态中成熟的HTTP模拟工具,通过持久化拦截器和延迟响应功能,为解决这些问题提供了基础能力。虽然Nock原生不直接支持WebSocket,但我们可以通过模拟HTTP升级请求和分块响应来实现订阅测试。
实现原理:HTTP模拟WebSocket通信
GraphQL订阅通常通过Upgrade请求将HTTP连接升级为WebSocket。我们可以分三个阶段模拟这个过程:
关键技术点包括:
- 使用Nock的.persist()方法保持拦截器活跃状态
- 通过delayBody控制数据推送时机
- 利用分块传输编码(Transfer-Encoding: chunked)模拟流式响应
实战步骤:构建模拟订阅测试
1. 建立持久WebSocket连接
首先模拟WebSocket升级握手,使用Nock创建持久化拦截器,匹配GraphQL订阅的初始HTTP请求:
const nock = require('nock');
// 模拟WebSocket升级请求
const subscriptionScope = nock('https://api.example.com', {
reqheaders: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Key': (value) => value !== undefined
}
})
.persist() // 保持拦截器不被移除
.get('/graphql')
.reply(101, '', {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Accept': ' simulated-accept-key'
});
这段代码创建了一个持久化拦截器,匹配所有包含WebSocket升级头的请求,并返回101状态码确认升级。关键配置:
persist()确保拦截器不会在首次匹配后被移除- 请求头验证确保只匹配WebSocket升级请求
- 响应头设置符合WebSocket协议规范
2. 模拟实时数据流推送
接下来实现分块数据推送,模拟服务器向客户端发送的实时更新。使用Nock的reply方法结合流响应:
// 模拟实时数据推送
function mockSubscriptionUpdates() {
const updates = [
'data: {"data":{"newMessage":{"id":"1","content":"Hello"}}}\n\n',
'data: {"data":{"newMessage":{"id":"2","content":"World"}}}\n\n'
];
return nock('https://api.example.com')
.persist()
.post('/graphql', {
query: 'subscription { newMessage { id content } }'
})
.delayBody(1000) // 延迟1秒发送第一个数据块
.reply(200, (uri, requestBody) => {
// 创建可读流模拟实时数据
const Readable = require('stream').Readable;
const stream = new Readable();
let index = 0;
const interval = setInterval(() => {
if (index < updates.length) {
stream.push(updates[index]);
index++;
} else {
clearInterval(interval);
stream.push(null); // 结束流
}
}, 1000); // 每秒推送一次更新
return stream;
}, {
'Content-Type': 'text/event-stream',
'Transfer-Encoding': 'chunked'
});
}
这个实现通过三个关键技术模拟实时数据流:
- 可读流(Readable Stream):逐块发送数据,模拟服务器推送
- 延迟响应:使用delayBody控制首次数据发送时间
- 分块传输:设置
Transfer-Encoding: chunked头,保持连接打开状态
3. 验证客户端行为
最后需要验证客户端是否正确处理订阅数据。结合Jest等测试框架,我们可以监听客户端状态变化并断言:
const { Client } = require('graphql-ws');
test('客户端接收实时消息', async () => {
// 启动模拟
const scope = mockSubscriptionUpdates();
const receivedMessages = [];
const client = new Client({
url: 'wss://api.example.com/graphql'
});
// 订阅消息
const subscription = client.iterate({
query: 'subscription { newMessage { id content } }'
});
// 收集接收的消息
for await (const event of subscription) {
if (event.data) {
receivedMessages.push(event.data.newMessage);
if (receivedMessages.length === 2) break;
}
}
// 验证结果
expect(receivedMessages).toEqual([
{ id: '1', content: 'Hello' },
{ id: '2', content: 'World' }
]);
// 清理
scope.done(); // 验证所有模拟请求都被调用
nock.cleanAll(); // 清除所有拦截器
}, 10000); // 延长超时时间适应延迟
测试过程中需要注意:
- 使用Nock的.done()方法验证模拟请求是否被正确触发
- 设置足够长的测试超时时间,适应延迟配置
- 测试结束后调用nock.cleanAll()清除拦截器,避免影响其他测试
高级技巧:处理复杂场景
模拟连接中断与重连
通过Nock的replyWithError方法模拟连接错误,测试客户端重连逻辑:
nock('https://api.example.com')
.persist()
.post('/graphql')
.times(1) // 只触发一次错误
.replyWithError({ code: 'ECONNRESET' })
.post('/graphql')
.persist()
.reply(200, ...); // 后续请求正常响应
动态生成订阅数据
利用Nock的函数式响应生成,根据客户端请求动态返回数据:
.reply(200, (uri, requestBody) => {
const { variables } = JSON.parse(requestBody);
// 根据订阅变量生成个性化数据
return generateDataForUser(variables.userId);
})
控制消息发送时序
结合delayConnection和delayBody精确控制消息发送节奏:
nock('https://api.example.com')
.post('/graphql')
.delayConnection(500) // 延迟建立连接
.delayBody(1000) // 延迟发送首个消息
.reply(200, stream);
完整测试示例
以下是一个完整的GraphQL订阅测试文件,位于examples/delay-response.js基础上修改而来:
const nock = require('nock');
const { Client } = require('graphql-ws');
describe('GraphQL订阅测试', () => {
afterEach(() => {
nock.cleanAll(); // 清除所有拦截器
});
it('应接收实时消息更新', async () => {
// 1. 模拟WebSocket升级
nock('https://api.example.com', {
reqheaders: {
'Upgrade': 'websocket'
}
})
.persist()
.get('/graphql')
.reply(101, '', {
'Upgrade': 'websocket',
'Connection': 'Upgrade'
});
// 2. 模拟订阅数据流
const updates = [
'data: {"data":{"newMessage":"第一条消息"}}\n\n',
'data: {"data":{"newMessage":"第二条消息"}}\n\n'
];
nock('https://api.example.com')
.persist()
.post('/graphql', {
query: 'subscription { newMessage }'
})
.reply(200, () => {
const stream = require('stream').Readable();
let i = 0;
const sendUpdate = () => {
if (i < updates.length) {
stream.push(updates[i]);
i++;
setTimeout(sendUpdate, 1000);
} else {
stream.push(null);
}
};
sendUpdate();
return stream;
}, { 'Content-Type': 'text/event-stream' });
// 3. 客户端订阅并验证
const client = new Client({ url: 'wss://api.example.com/graphql' });
const messages = [];
for await (const event of client.iterate({
query: 'subscription { newMessage }'
})) {
if (event.data) {
messages.push(event.data.newMessage);
if (messages.length === 2) break;
}
}
expect(messages).toEqual(['第一条消息', '第二条消息']);
expect(nock.isDone()).toBe(true); // 验证所有模拟都被调用
}, 15000);
});
总结与最佳实践
使用Nock模拟GraphQL订阅的核心价值在于:将实时通信转化为可控的本地测试。通过本文介绍的方法,你可以:
最佳实践建议:
- 将通用订阅模拟逻辑抽象为测试工具,如tests/test_socket.js所示
- 对每个订阅类型编写独立测试,避免模拟冲突
- 使用nock.back录制真实订阅数据,生成测试 fixtures
这种测试方法已被验证可有效提高实时应用的测试稳定性。下一篇我们将探讨如何结合Nock与React Testing Library测试前端订阅组件,敬请关注。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



