NestJS+TypeScript单元测试报Cannot read property 'getById' of undefined
问题描述
工作中使用NestJS开发,对单元测试不熟练,开发的图片CRUD操作模块的单元测试无法运行,相关代码如下:
media.gateway.ts
import { Injectable } from '@nestjs/common'; import { MediaEntity } from '../entities/media.entity'; import { DBService } from '../../services/db/db.service'; import { NotFoundException } from '../../exceptions/not-found.exception'; @Injectable() export class MediaGateway { private mediaTable: string = 'media'; constructor( private readonly db: DBService, ) { } public async getById(mediaId: number): Promise<MediaEntity> { const mediaRaw: any = await this.db.fetchOne('\ SELECT id, name, type, private, created, users_id, media_storage_id \ FROM ' + this.mediaTable + ' \ WHERE id = ?', [ mediaId ]); if (!mediaRaw) { throw new NotFoundException('Media not found'); } return this.toMediaEntity(mediaRaw); } private toMediaEntity(media: any): MediaEntity { return new MediaEntity( media.id, media.name, media.type ? media.type.toString() : null, media.private === 1, media.media_storage_id, media.users_id, new Date(media.created), ); } }
单元测试代码 media.gateway.spec.ts
import { Fixture } from '../../../../test/fixture'; import { Test } from '@nestjs/testing'; import { DBServiceTestProvider } from '../../../../test/db.service.test.provider'; import { MediaEntity, MediaType } from '../entities/media.entity'; import { MediaGateway } from './media.gateway'; describe('Media Gateway', () => { let gateway: MediaGateway; beforeEach(async () => { await Fixture.clearAndInit(); const module = await Test.createTestingModule({ providers: [ MediaGateway, DBServiceTestProvider, ], }).compile(); gateway = module.get<MediaGateway>(MediaGateway); }); describe('Get existing media by id', async () => { it('Should return media entity', async () => { const media: MediaEntity = await gateway.getById(1); expect(media).toMatchObject(new MediaEntity( 1, 'Media Name', MediaType.PHOTO, false, 1, 1, new Date('2019-04-25 19:00:05'), )); }); }); });
首次运行报错
TypeError: Cannot read property 'getById' of undefined 25 | describe('Get existing media by id', async () => { 26 | it('Should return media entity', async () => { > 27 | const media: MediaEntity = await gateway.getById(1); | ^ 28 | expect(media).toMatchObject(new MediaEntity( 29 | 1, 30 | 'Media Name',
VSCode无代码错误提示,media.gateway.ts引用路径正确。
补充简化测试代码
import { Fixture } from '../../../../test/fixture'; import { Test } from '@nestjs/testing'; import { DBServiceTestProvider } from '../../../../test/db.service.test.provider'; import { MediaEntity, MediaType } from '../entities/media.entity'; import { MediaGateway } from './media.gateway2'; describe('Media Gateway', () => { let gateway: MediaGateway; beforeEach(async () => { await Fixture.clearAndInit(); const module = await Test.createTestingModule({ providers: [ MediaGateway, DBServiceTestProvider, ], }).compile(); gateway = module.get<MediaGateway>(MediaGateway); }); it('should be defined', () => { expect(gateway).toBeDefined(); }); });
简化测试运行报错
> tslint -p tsconfig.json -c tslint.json && jest --no-cache --runInBand "src/core/medias/gateways/media.gateway2.spec.ts" FAIL src/core/medias/gateways/media.gateway2.spec.ts (6.054s) Media Gateway ✕ should be defined (64ms) ● Media Gateway › should be defined ER_DUP_KEYNAME: Duplicate key name 'value_UNIQUE' at Query.Object.<anonymous>.Sequence._packetToError (../node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14) at Query.Object.<anonymous>.Query.ErrorPacket (../node_modules/mysql/lib/protocol/sequences/Query.js:79:18) at Protocol.Object.<anonymous>.Protocol._parsePacket (../node_modules/mysql/lib/protocol/Protocol.js:291:23) at Parser._parsePacket (../node_modules/mysql/lib/protocol/Parser.js:433:10) at Parser.write (../node_modules/mysql/lib/protocol/Parser.js:43:10) at Protocol.Object.<anonymous>.Protocol.write (../node_modules/protocol/Protocol.js:38:16) at Socket.<anonymous> (../node_modules/mysql/lib/Connection.js:88:28) at Socket.<anonymous> (../node_modules/mysql/lib/Connection.js:526:10) -------------------- at Protocol.Object.<anonymous>.Protocol._enqueue (../node_modules/mysql/lib/protocol/Protocol.js:144:48) at Connection.query (../node_modules/mysql/lib/Connection.js:198:25) at Promise (../node_modules/postgrator/lib/MysqlClient.js:57:20) at MysqlClient._runQuery (../node_modules/postgrator/lib/MysqlClient.js:56:12) at MysqlClient.runQuery (../node_modules/postgrator/lib/Client.js:58:19) at sequence.then.then.then (../node_modules/postgrator/postgrator.js:217:37) ● Media Gateway › should be defined expect(received).toBeDefined() Received: undefined 24 | 25 | it('should be defined', () => { > 26 | expect(gateway).toBeDefined(); | ^ 27 | }); 28 | // describe('Get existing media by id', async () => { 29 | // it('Should return media entity', async () => { at Object.it (core/medias/gateways/media.gateway2.spec.ts:26:25) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 0 total Time: 6.078s
解决方案
根因分析
你的问题和MediaGateway本身的代码、引用路径无关,问题出在beforeEach生命周期的执行流程:await Fixture.clearAndInit()执行时抛出了数据库错误,直接中断了后续逻辑,测试模块没有完成编译,gateway变量从未被赋值,所以才会报undefined相关错误。报错中首先出现的ER_DUP_KEYNAME: Duplicate key name 'value_UNIQUE'是明确的MySQL数据库错误,指建表/加索引时重复创建了同名的唯一键。
解决步骤
1. 修复数据库初始化错误
这是优先级最高的问题:
- 检查你的数据库迁移脚本,确认是否有重复定义
value_UNIQUE唯一键的逻辑,比如同一个表的建表语句中两次添加同名唯一索引,或者加索引的语句没有做存在性判断。 - 检查
Fixture.clearAndInit()的实现逻辑,确认是否真的完整清空了测试库的所有表、包括迁移工具的记录表(比如postgrator的schemaversion表),如果漏删迁移记录表,会导致每次初始化都重复执行所有迁移脚本,触发重复建索引的错误。 - 可以手动清空测试库的所有表,再重新运行测试验证错误是否消失。
2. 验证模块实例化逻辑
可以临时注释掉初始化逻辑,排除数据库的影响,确认MediaGateway的实例化是否正常:
beforeEach(async () => { // 临时注释,验证模块逻辑 // await Fixture.clearAndInit(); const module = await Test.createTestingModule({ providers: [ MediaGateway, DBServiceTestProvider, ], }).compile(); gateway = module.get<MediaGateway>(MediaGateway); });
如果注释后gateway可以正常实例化,即可完全确认问题出在Fixture初始化流程。
3. 单元测试优化建议
如果只是对MediaGateway做单元测试,不需要依赖真实数据库,可以直接mock DBService,避免这类数据库环境问题,同时提升测试运行速度:
// 模拟DBService返回值 const mockDBService = { fetchOne: jest.fn().mockResolvedValue({ id: 1, name: 'Media Name', type: MediaType.PHOTO, private: 0, media_storage_id: 1, users_id: 1, created: '2019-04-25 19:00:05' }) } const module = await Test.createTestingModule({ providers: [ MediaGateway, // 替换真实的DBService为模拟对象 { provide: DBService, useValue: mockDBService } ] }).compile();
内容的提问来源于stack exchange,提问作者djchals
相关产品推荐
相关产品推荐

