使用Jest测试GraphQL Yoga订阅遇阻塞,触发超时错误
GraphQL Yoga订阅Jest测试超时问题解决
问题描述
我一直在尝试用Jest编写GraphQL Yoga订阅的测试用例,但始终无法正常运行,持续触发超时错误,无法定位问题根源。
服务端代码
server.js
import {createYoga, createPubSub} from 'graphql-yoga' import { makeExecutableSchema} from '@graphql-tools/schema'; import {applyMiddleware} from 'graphql-middleware' import resolvers from './resolvers/index' import {createServer} from 'node:http' import gprf from 'graphql-parse-relation-fields' import dbRelationalFields from "./helpers/db-relational-fields"; import userFragmentMiddleware from './middleware/userFragmentMiddleware' import typeDefs from './typeDefs'; import prismaContext from './prismaContext'; const pubsub = createPubSub() const schema = makeExecutableSchema({typeDefs, resolvers}) const schemaWithMiddleware = applyMiddleware(schema, userFragmentMiddleware) const yoga = createYoga({ schema: schemaWithMiddleware, context({request}){ return { pubsub, gprf, prisma:prismaContext, dbRelationalFields, request } }, }) const server = createServer(yoga) export {server}
index.js
import {server} from './server' import * as dotenv from 'dotenv' dotenv.config() const main = async() => { let port = process.env.PORT || 4000 server.listen(port, ()=>{ console.log(`the server is up on port: ${port}`) }) } main();
Jest相关代码
getClient.js
import { ApolloClient, InMemoryCache, createHttpLink, split } from '@apollo/client'; import { getMainDefinition } from '@apollo/client/utilities'; import { setContext } from '@apollo/client/link/context'; import { SubscriptionClient } from 'subscriptions-transport-ws'; import WebSocket from 'ws'; const getClient = (jwt = '') => { const wsLink = new SubscriptionClient( 'ws://localhost:4000/graphql', { reconnect: true, connectionParams: { authToken: !!jwt ? jwt : null } }, WebSocket ); let uri = 'http://localhost:4000/graphql'; let httpLink = createHttpLink({ uri }); const authLink = setContext((_, { headers }) => { return { headers: { ...headers, authorization: !!jwt ? `Bearer ${jwt}` : '' } }; }); const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, authLink.concat(httpLink) ); return new ApolloClient({ link: splitLink, cache: new InMemoryCache(), request(operation) { if (!!jwt === true) { operation.setContext({ headers: { authorization: `Bearer ${jwt}` } }); } } }); }; export { getClient as default };
comment-operation.js
import {gql } from '@apollo/client'; const deleteComment = gql` mutation deleteComment($id: ID!, $postId: ID!){ deleteComment(id: $id, postId: $postId){ id text author { id } post { id } } } ` const subscribeToComments = gql` subscription subscribeToComments($postId: ID!){ comment(postId: $postId){ mutation data { id text updateCheckField } } } ` export {deleteComment, subscribeToComments}
comment.spec.js(原测试代码)
import 'cross-fetch/polyfill' import React from 'react'; import ReactDOM from 'react-dom'; import bcrypt from 'bcryptjs' import { execute, subscribe } from '@apollo/client/link/core' import { gql } from '@apollo/client'; import PrismaContext from '../src/prismaContext' import { v4 as uuidv4 } from 'uuid'; import seedDatabase, {userOne, userTwo} from './utils/seedDatabase' import getClient, {wsLink} from './utils/getClient' import {deleteComment, subscribeToComments} from './utils/comment-operations' import {deletePost} from './utils/post-operations' const client = getClient(); beforeEach(seedDatabase) test('should subscribe to comments for a post', async(done)=>{ const client = getClient(userTwo.jwt) const variables = { postId: userTwo.posts[0]["id"] } const subscription = client.subscribe({ query: subscribeToComments, variables}).subscribe({ next: (result) => { expect(result.data.comment.mutation).toBe('DELETED') expect(result.data.comment.data.updateCheckField).toBe(`${userTwo.comments[1].id}${userOne.user.id}${userTwo.posts[0].id}`) subscription.unsubscribe(); done(); }, }); const client1 = getClient(userOne.jwt) const variables1 = { id: userTwo.comments[1]["id"], postId: userTwo.posts[0]["id"] } client1.mutate({mutation: deleteComment, variables: variables1}) .then((response)=>{}) .catch((err)=> console.log('nawaooo', err)) })
错误信息
抛出错误:"测试等待
done()调用时超出了5000ms超时限制。如果这是一个耗时较长的测试,请为该测试设置更长的超时时间。"
解决方案
1. 确保测试生命周期内服务正确启停
原测试代码未在测试前启动服务,导致客户端可能无法连接WebSocket。需在beforeEach中启动服务,afterEach中关闭服务:
import { server } from '../src/server' beforeEach(async () => { await seedDatabase() // 等待服务启动完成 await new Promise(resolve => server.listen(4000, resolve)) }) afterEach(async () => { // 等待服务关闭完成 await new Promise(resolve => server.close(resolve)) })
2. 处理异步操作的错误与顺序
- 用
await等待mutation执行完成,避免异步顺序混乱 - 在订阅回调中添加错误捕获,确保断言失败时也能调用
done() - 处理订阅的
error回调,避免连接错误导致超时
3. 增加测试超时时间
订阅测试通常需要更长时间,在测试定义中设置超时参数:
test('should subscribe to comments for a post', async(done)=>{ // ... 测试逻辑 }, 10000) // 设置10秒超时
4. 验证订阅解析器的事件发布
确认deleteComment mutation执行后,是否正确调用pubsub.publish触发comment订阅事件,示例resolver代码:
// resolvers/comment.js const deleteComment = async (_, { id, postId }, { pubsub, prisma }) => { const comment = await prisma.comment.delete({ where: { id } }) // 发布订阅事件 await pubsub.publish('COMMENT', { comment: { mutation: 'DELETED', data: { ...comment, updateCheckField: `${id}${comment.authorId}${postId}` } } }) return comment }
修改后的完整测试代码
import 'cross-fetch/polyfill' import { v4 as uuidv4 } from 'uuid'; import seedDatabase, {userOne, userTwo} from './utils/seedDatabase' import getClient from './utils/getClient' import {deleteComment, subscribeToComments} from './utils/comment-operations' import { server } from '../src/server' beforeEach(async () => { await seedDatabase() await new Promise(resolve => server.listen(4000, resolve)) }) afterEach(async () => { await new Promise(resolve => server.close(resolve)) }) test('should subscribe to comments for a post', async(done) => { const client = getClient(userTwo.jwt) const variables = { postId: userTwo.posts[0]["id"] } const subscription = client.subscribe({ query: subscribeToComments, variables }).subscribe({ next: (result) => { try { expect(result.data.comment.mutation).toBe('DELETED') expect(result.data.comment.data.updateCheckField).toBe(`${userTwo.comments[1].id}${userOne.user.id}${userTwo.posts[0].id}`) subscription.unsubscribe() done() } catch (err) { done(err) } }, error: (err) => { done(err) } }) try { const client1 = getClient(userOne.jwt) const variables1 = { id: userTwo.comments[1]["id"], postId: userTwo.posts[0]["id"] } await client1.mutate({mutation: deleteComment, variables: variables1}) } catch (err) { done(err) } }, 10000)
内容的提问来源于stack exchange,提问作者Osas
相关产品推荐
相关产品推荐

