TypeGraphQL+Typegoose+Express应用单元测试方案咨询
针对Express + TypeGraphQL + TypeGoose栈的单元测试落地方案
前置依赖与环境配置
先安装测试相关依赖,执行命令:npm i -D jest ts-jest @types/jest mongodb-memory-server apollo-server-testing
在项目根目录新建jest.config.js,基础配置如下:
/** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/**/*.test.ts'], verbose: true, forceExit: true, clearMocks: true, resetMocks: true, restoreMocks: true, }
在package.json中添加测试脚本:
{ "scripts": { "test": "jest" } }
TypeGoose Schema 单元测试写法
Schema测试核心覆盖三类逻辑:字段约束、中间件钩子、自定义方法,测试时用内存MongoDB替代真实数据库,避免污染业务数据。
以项目里的Admin Schema为例,测试文件admin.schema.test.ts示例:
import { MongoMemoryServer } from 'mongodb-memory-server' import mongoose from 'mongoose' import bcrypt from 'bcrypt' import { AdminModel } from './admin.schema' let mongo: MongoMemoryServer // 测试前启动内存Mongo、建立连接 beforeAll(async () => { mongo = await MongoMemoryServer.create() const uri = mongo.getUri() await mongoose.connect(uri) }) // 每个用例跑完清空Admin集合 beforeEach(async () => { await AdminModel.deleteMany({}) jest.clearAllMocks() }) // 所有用例跑完断开连接、停掉内存服务 afterAll(async () => { await mongoose.connection.close() await mongo.stop() }) describe('Admin Schema测试', () => { it('保存文档时自动哈希明文密码', async () => { const plainPwd = 'test123456' const admin = await AdminModel.create({ firstname: 'test', email: 'test@admin.com', password: plainPwd }) // 校验存到库的密码不是明文 expect(admin.password).not.toBe(plainPwd) // 校验哈希值可以和明文匹配 const isMatch = await bcrypt.compare(plainPwd, admin.password) expect(isMatch).toBe(true) }) it('email字段加了唯一约束,重复邮箱保存会报错', async () => { await AdminModel.create({ firstname: 'test1', email: 'repeat@admin.com', password: '123456' }) await expect( AdminModel.create({ firstname: 'test2', email: 'repeat@admin.com', password: '123456' }) ).rejects.toThrow() }) it('created字段会自动生成默认值', async () => { const admin = await AdminModel.create({ firstname: 'test', email: 'test@admin.com', password: '123456' }) expect(admin.created).toBeDefined() }) })
TypeGraphQL Resolver 单元测试写法
前置修改提示:当前AdminResolver构造函数内直接实例化AdminService,会导致测试时无法替换依赖做Mock,建议先调整为依赖注入形式,去掉构造函数内的this.adminService = new AdminService(),实例化Resolver时手动传入service实例即可,调整后构造函数如下:
constructor(private adminService: AdminService) {}
Resolver测试分两种粒度,可按需选择:
1. 纯单元测试(速度快,适合逻辑校验)
直接实例化Resolver类,Mock掉所有依赖的Service、上下文对象,直接调用Resolver方法校验入参出参逻辑,示例admin.resolver.test.ts:
import AdminResolver from './admin.resolver' import AdminService from '../../service/Admin/admin.service' // Mock整个AdminService jest.mock('../../service/Admin/admin.service') const mockAdminService = AdminService as jest.MockedClass<typeof AdminService> describe('Admin Resolver纯单元测试', () => { let resolver: AdminResolver beforeEach(() => { // 每个用例重新实例化Resolver,传入mock的service resolver = new AdminResolver(new mockAdminService()) }) it('meAdmin查询返回上下文里的admin信息', () => { const mockAdmin = { _id: 'testid', email: 'test@admin.com', firstname: 'test' } const mockCtx = { admin: mockAdmin, res: {} } const result = resolver.meAdmin(mockCtx as any) expect(result).toEqual(mockAdmin) }) it('loginAdmin变更调用service的loginAdmin方法并返回结果', async () => { const mockInput = { email: 'test@admin.com', password: '123456' } const mockToken = 'test-jwt-token' const mockCookieFn = jest.fn() const mockCtx = { res: { cookie: mockCookieFn } } // mock service方法返回值 mockAdminService.prototype.loginAdmin.mockResolvedValue(mockToken) const result = await resolver.loginAdmin(mockInput, mockCtx as any) // 校验service被正确调用 expect(mockAdminService.prototype.loginAdmin).toHaveBeenCalledWith(mockInput, mockCtx) // 校验返回值正确 expect(result).toBe(mockToken) }) })
2. 带GraphQL执行层的测试(更贴近真实请求,适合接口联调校验)
通过apollo-server-testing创建测试用的Apollo Server实例,构造测试Schema后直接发送GraphQL请求,校验完整链路逻辑,示例:
import { ApolloServer, gql } from 'apollo-server-express' import { createTestClient } from 'apollo-server-testing' import AdminResolver from './admin.resolver' import { buildSchema } from 'type-graphql' const LOGIN_MUTATION = gql` mutation LoginAdmin($input: AdminLoginInput!) { loginAdmin(input: $input) } ` const ME_QUERY = gql` query MeAdmin { meAdmin { _id email firstname } } ` describe('Admin Resolver接口测试', () => { it('登录接口成功返回token并设置cookie', async () => { const mockCookieFn = jest.fn() const schema = await buildSchema({ resolvers: [AdminResolver] }) const server = new ApolloServer({ schema, context: () => ({ res: { cookie: mockCookieFn }, admin: null }) }) const { mutate } = createTestClient(server) const res = await mutate({ query: LOGIN_MUTATION, variables: { input: { email: 'test@admin.com', password: '123456' } } }) expect(res.data?.loginAdmin).toBeDefined() expect(mockCookieFn).toHaveBeenCalledWith('accessToken', expect.any(String), expect.objectContaining({ httpOnly: true, sameSite: 'none' })) }) })
测试注意事项
- 涉及第三方依赖(比如bcrypt、jwt、nodemailer)的纯单元测试,可以直接Mock对应方法,大幅提升测试执行速度,集成测试场景再走真实逻辑
- 内存Mongo启动需要等待连接建立完成再执行用例,否则会出现随机连接报错
- 每个测试用例执行完要清空集合、重置Mock,避免用例之间的数据和状态互相干扰
- 上下文对象里的req/res属性如果没有被当前测试逻辑用到,可以只Mock用到的字段(比如只用到res.cookie就只实现这个方法),不用构造完整的req/res对象
内容的提问来源于stack exchange,提问作者Danial Bayat
相关产品推荐
相关产品推荐

