Nestjs 单元测试 - 模拟方法保护

Dan*_*iel 9 unit-testing dependency-injection mocking jestjs nestjs

我已经开始使用 NestJS 并且有一个关于模拟单元测试的守卫的问题。我正在尝试测试一个基本的 HTTP controller,它有一个方法 Guard 附加到它。

当我向 Guard 注入服务时,我的问题就开始了(我需要ConfigServiceGuard 的服务)。

运行测试时,DI 无法解析 Guard

  ? AppController › root › should return "Hello World!"

    Nest can't resolve dependencies of the ForceFailGuard (?). Please make sure that the argument at index [0] is available in the _RootTestModule context.
Run Code Online (Sandbox Code Playgroud)

我的力量失败守卫:

  ? AppController › root › should return "Hello World!"

    Nest can't resolve dependencies of the ForceFailGuard (?). Please make sure that the argument at index [0] is available in the _RootTestModule context.
Run Code Online (Sandbox Code Playgroud)

规范文件:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { ConfigService } from './config.service';

@Injectable()
export class ForceFailGuard implements CanActivate {

  constructor(
    private configService: ConfigService,
  ) {}

  canActivate(context: ExecutionContext) {
    return !this.configService.get().shouldFail;
  }
}
Run Code Online (Sandbox Code Playgroud)

我无法找到有关此问题的示例或文档。我错过了什么还是这是一个真正的问题?

感谢任何帮助,谢谢。

小智 9

提供的示例存储库存在 3 个问题:

  1. Nestjs v6.1.1 中存在一个错误.overrideGuard()- 请参阅https://github.com/nestjs/nest/issues/2070

    我已经确认它已在 6.5.0 中修复。

  2. ForceFailGuard在 中providers,但它的依赖项 ( ConfigService) 在 created 中不可用TestingModule

    如果您想模拟ForceFailGuard,只需将其从中删除providers并让其.overrideGuard()完成其工作。

  3. mock_ForceFailGuardCanActivate作为财产而不是canActivate.

工作示例(nestjs v6.5.0):

import { CanActivate } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ForceFailGuard } from './force-fail.guard';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const mock_ForceFailGuard: CanActivate = { canActivate: jest.fn(() => true) };

    const app: TestingModule = await Test
      .createTestingModule({
        controllers: [AppController],
        providers: [
          AppService,
        ],
      })
      .overrideGuard(ForceFailGuard).useValue(mock_ForceFailGuard)
      .compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});
Run Code Online (Sandbox Code Playgroud)


vic*_*chi 7

如果除了控制器单元测试之外,您还需要/想要对自定义防护实现进行单元测试,您可以进行类似于下面的测试,以预期错误等

// InternalGuard.ts
@Injectable()
export class InternalTokenGuard implements CanActivate {
  constructor(private readonly config: ConfigService) {
  }

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const token = this.config.get("internalToken");

    if (!token) {
      throw new Error(`No internal token was provided.`);
    }

    const request = context.switchToHttp().getRequest();
    const providedToken = request.headers["authorization"];

    if (token !== providedToken) {
      throw new UnauthorizedException();
    }

    return true;
  }
}
Run Code Online (Sandbox Code Playgroud)

还有你的规格文件

// InternalGuard.spec.ts
beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    controllers: [],
    providers: [
      InternalTokenGuard,
      {
        provide: ConfigService,
        useValue: {
          get: jest.fn((key: string) => {
            if (key === "internalToken") {
              return 123;
            }
            return null;
          })
        }
      }
    ]
  }).compile();

  config = module.get<ConfigService>(ConfigService);
  guard = module.get<InternalTokenGuard>(InternalTokenGuard);
});

it("should throw UnauthorizedException when token is not Bearer", async () => {
  const context = {
    getClass: jest.fn(),
    getHandler: jest.fn(),
    switchToHttp: jest.fn(() => ({
      getRequest: jest.fn().mockReturnValue({
        headers: {
          authorization: "providedToken"
        }
      })
    }))
  } as any;

  await expect(guard.canActivate(context)).rejects.toThrow(
    UnauthorizedException
  );
  expect(context.switchToHttp).toHaveBeenCalled();
});
Run Code Online (Sandbox Code Playgroud)