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

NestJS测试中使用HttpService.get捕获Axios错误响应如何避免超时?

问题描述

原有代码可通过playlistID获取YouTube播放列表最新上传视频,但修改代码以捕获无效playlistID的错误响应后,测试用例超时失败。

原测试代码(get_latest_upload.service.spec.ts)

import { Test, TestingModule } from '@nestjs/testing';
import { GetLatestUploadService } from './get_latest_upload.service';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { createMock, DeepMocked } from '@golevelup/ts-jest';

describe('GetLatestUploadService', () => {
  let service: GetLatestUploadService;
  let httpService: jest.Mocked<HttpService>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [],
      providers: [
        { provide: HttpService, useValue: createMock<HttpService>() },
        ConfigService
      ],
      controllers: [
        GetLatestUploadService
      ],
    }).compile();

    service = module.get<GetLatestUploadService>(GetLatestUploadService);
    httpService = module.get(HttpService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getLatestUpload(id)', () => {
    it('sholud return youtube video id', async () => {
      const playlistID = "PLcGkkXtask_eafKUbnGPBuCsLt2LN8SQe";
      const testData = require(`../../test/playlists/${playlistID}.json`);
      const testID = testData.items[0].snippet.playlistId;
      const videoID = testData.items[0].snippet.resourceId.videoId;
      httpService.axiosRef.mockResolvedValueOnce({
        data: testData,
        headers: {},
        config: { url: '' },
        status: 200,
        statusText: '',
      });
      const getLatestUpload = service.getLatestUpload(testID);
      await expect(getLatestUpload).resolves.toBe(videoID);
    });
  });
});

原服务代码(get_latest_upload.service.ts)

import { Inject, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom, catchError } from 'rxjs';
import { AxiosError } from 'axios';
import { AxiosResponse } from 'axios';

@Injectable()
export class GetLatestUploadService {
  @Inject(ConfigService)
  public config: ConfigService;
  private readonly logger = new Logger(GetLatestUploadService.name);
  constructor(private readonly httpService: HttpService) {}

  async getLatestUpload(playlistID): Promise<string> {
    const apiKey: string = this.config.get('YOUTUBE_API_KEY');
    const part = "snippet%2C+id";  
    const url = `https://www.googleapis.com/youtube/v3/playlistItems?part=${part}&playlistId=${playlistID}&key=${apiKey}&order=date&maxResults=1`;
    const { data } = await this.httpService.axiosRef({
      url: url,
      method: `GET`,
    });    
    const videoId = data.items[0].snippet.resourceId.videoId;
    return videoId;
  }
}

修改后的服务代码(添加错误捕获)

import { Inject, Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom, catchError } from 'rxjs';
import { AxiosError } from 'axios';

@Injectable()
export class GetLatestUploadService {
  @Inject(ConfigService)
  public config: ConfigService;
  private readonly logger = new Logger(GetLatestUploadService.name);
  
  constructor(private readonly httpService: HttpService) {}

  async getLatestUpload(playlistID: string): Promise<string> {
    const apiKey: string = this.config.get('YOUTUBE_API_KEY');
    const part = "snippet%2C+id";  
    const url = `https://www.googleapis.com/youtube/v3/playlistItems?part=${part}&playlistId=${playlistID}&key=${apiKey}&order=date&maxResults=1`;
    
    const { data } = await firstValueFrom(
      this.httpService.get(url).pipe(
        catchError((error: AxiosError) => {
          this.logger.error(error.response?.data || error.message);
          throw new HttpException(
            error.response?.data?.error?.message || 'Failed to fetch playlist',
            error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
          );
        }),
      ),
    );    
    return data.items[0].snippet.resourceId.videoId;
  }
}

测试超时原因及修复

测试超时是因为服务代码改用了httpService.get(),但测试中依然mock的是httpService.axiosRef,导致mock未命中,httpService.get()返回的Observable一直处于未完成状态,最终触发超时。

修改测试代码,改为mockhttpService.get()方法:

import { Test, TestingModule } from '@nestjs/testing';
import { GetLatestUploadService } from './get_latest_upload.service';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { createMock } from '@golevelup/ts-jest';
import { of } from 'rxjs';
import { HttpException } from '@nestjs/common';
import { AxiosError } from 'axios';

describe('GetLatestUploadService', () => {
  let service: GetLatestUploadService;
  let httpService: jest.Mocked<HttpService>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        GetLatestUploadService,
        { provide: HttpService, useValue: createMock<HttpService>() },
        ConfigService
      ],
    }).compile();

    service = module.get<GetLatestUploadService>(GetLatestUploadService);
    httpService = module.get(HttpService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getLatestUpload(id)', () => {
    it('should return youtube video id', async () => {
      const playlistID = "PLcGkkXtask_eafKUbnGPBuCsLt2LN8SQe";
      const testData = require(`../../test/playlists/${playlistID}.json`);
      const testID = testData.items[0].snippet.playlistId;
      const videoID = testData.items[0].snippet.resourceId.videoId;
      
      // 改为mock httpService.get,返回Observable
      httpService.get.mockReturnValueOnce(of({
        data: testData,
        headers: {},
        config: { url: '' },
        status: 200,
        statusText: '',
      }));
      
      const result = await service.getLatestUpload(testID);
      expect(result).toBe(videoID);
    });

    it('should throw error when playlist ID is invalid', async () => {
      const invalidPlaylistID = 'invalid-id';
      const mockErrorResponse = {
        response: {
          status: 404,
          data: {
            error: {
              message: "The playlist identified with the request's <code>playlistId</code> parameter cannot be found."
            }
          }
        }
      } as AxiosError;

      // mock httpService.get抛出错误
      httpService.get.mockImplementationOnce(() => {
        throw mockErrorResponse;
      });

      await expect(service.getLatestUpload(invalidPlaylistID)).rejects.toThrow(HttpException);
    });
  });
});

关键修改点

  1. 测试mock对象对齐:将httpService.axiosRef.mockResolvedValueOnce改为httpService.get.mockReturnValueOnce,并返回of()包装的Observable(Nest的HttpService.get返回Observable)。
  2. 错误处理优化:将抛出字符串错误改为抛出Nest标准的HttpException,符合框架异常处理规范,便于全局异常过滤器统一处理。
  3. 添加错误场景测试:新增测试用例验证无效playlistID时的错误抛出逻辑。

内容的提问来源于stack exchange,提问作者OB7DEV

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 19:54:53