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

NestJS单元测试:为何不用提供者令牌无法注入模拟AuthService?

问题描述

我在NestJS沙盒项目中编写单元测试时遇到一个问题:注入了authService的AuthImplementation类,在测试中直接模拟AuthService时,authService会显示为undefined;但使用提供者令牌注入后,模拟服务能正常工作。两种注入方式在实际运行环境中都正常,只有单元测试出现这个问题。

相关代码

auth.module.ts

@Module({
    imports: [DatabaseModule],
    controllers: [AuthController],
    providers: [
        {
            provide: 'UserRepository',
            useFactory: (databaseAdapterService: DatabaseAdapterService) =>
                databaseAdapterService.getUserRepository(),
            inject: [DatabaseAdapterService],
        },
        {
            provide: AuthService,
            useFactory: (userRepository: UserRepository) => {
                return new AuthService(userRepository);
            },
            inject: ['UserRepository'],
        },
        {
            provide: AuthImplementation,
            useFactory: (authService: IAuthService) =>
                new AuthImplementation(authService),
            inject: [AuthService],
        },
    ]
})

auth.implementation.ts

import { Inject } from '@nestjs/common';
import { User } from 'src/domain/entities/user.entity';
import { AUTH_SERVICE, IAuthService } from 'src/domain/ports/auth-service.interface';

export class AuthImplementation {
    constructor(@Inject(AUTH_SERVICE) private readonly authService: IAuthService) { }
    
    // 下面这种不用提供者令牌的写法,在单元测试中会导致authService为undefined
    // constructor(private readonly authService: IAuthService) { }

    async register(username: string, password: string): Promise<User> {
        return this.authService.register(username, password);
    }

    async login(username: string, password: string): Promise<User | null> {
        return this.authService.login(username, password);
    }
}

auth.implementation.spec.ts

describe('AuthImplementation', () => {
    let authImplementation: AuthImplementation;
    let mockAuthService: jest.Mocked<IAuthService>;

    beforeEach(async () => {
        mockAuthService = {
            register: jest.fn(),
            login: jest.fn(),
        };

        const module: TestingModule = await Test.createTestingModule({
            providers: [
                AuthImplementation,
                {
                    // 这种写法不生效
                    //provide: AuthService,

                    // 这种写法正常工作
                    provide: AUTH_SERVICE,
                    useValue: mockAuthService,
                },
            ],
        }).compile();

        authImplementation = module.get<AuthImplementation>(AuthImplementation);
    });

    describe('register', () => {
        it('should register a user and return the user object', async () => {
            const username = 'testuser';
            const password = 'testpass';
            const mockUser = new User(new ObjectId(), username, password);

            mockAuthService.register.mockResolvedValue(mockUser);

            const result = await authImplementation.register(username, password);

            expect(mockAuthService.register).toHaveBeenCalledWith(username, password);
            expect(result).toBe(mockUser);
        });
    });

我的问题是:

  1. 是否可以不用定义export const AUTH_SERVICE = Symbol('AUTH_SERVICE');这样的提供者令牌,直接在测试模块中引用AuthService?
  2. 为何使用provide: AUTH_SERVICE能正常工作,而provide: AuthService会导致AuthImplementation中的authService显示为undefined?

问题解答

为什么provide: AuthService不生效?

核心原因是你在AuthImplementation的构造函数里明确用@Inject(AUTH_SERVICE)指定了注入令牌,而非依赖AuthService类本身作为注入标识。

NestJS的依赖注入系统完全基于「令牌匹配」:

  • 当构造函数中使用@Inject(TOKEN)时,Nest会严格查找provide值等于TOKEN的提供者
  • 只有当你不写@Inject时,Nest才会默认把类本身作为注入令牌(比如constructor(private authService: AuthService))

你当前的代码里,AuthImplementation明确要求注入AUTH_SERVICE令牌对应的实例,所以测试模块里如果只提供provide: AuthService的模拟,Nest找不到匹配的令牌,自然会注入undefined。

能不能不用令牌直接引用AuthService?

可以,但需要同步调整三处代码:

  1. 修改AuthImplementation的构造函数:去掉@Inject(AUTH_SERVICE),直接依赖AuthService类(需确保AuthService实现了IAuthService接口):
// auth.implementation.ts
import { AuthService } from './path-to-auth-service'; // 导入实际的AuthService类

export class AuthImplementation {
    constructor(private readonly authService: AuthService) { } // 直接注入类

    // 业务方法保持不变
    async register(username: string, password: string): Promise<User> {
        return this.authService.register(username, password);
    }
}
  1. 调整测试模块的提供者:
// auth.implementation.spec.ts
beforeEach(async () => {
    mockAuthService = {
        register: jest.fn(),
        login: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
        providers: [
            AuthImplementation,
            {
                provide: AuthService, // 现在用AuthService类作为令牌
                useValue: mockAuthService,
            },
        ],
    }).compile();

    authImplementation = module.get<AuthImplementation>(AuthImplementation);
});
  1. 同步更新AuthModule中的AuthImplementation提供者:
// auth.module.ts
{
    provide: AuthImplementation,
    useFactory: (authService: AuthService) => // 这里改为AuthService类型
        new AuthImplementation(authService),
    inject: [AuthService], // 注入令牌改为AuthService类
},

额外说明

如果你想保持「依赖接口而非具体类」的设计(符合依赖倒置原则),那使用提供者令牌是更规范的做法。这种情况下,测试时必须用对应的令牌(AUTH_SERVICE)来提供模拟,否则Nest无法匹配到正确的依赖。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 17:14:50