You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 11:37:12