5

I'm trying to unit test this controller and mock away the services/repositories that it needs.

@Controller('auth')
export class AuthController {
    constructor(
        private readonly authService: AuthService,
        private readonly usersService: UsersService,
    ) {}

    @Post('register')
    public async registerAsync(@Body() createUserModel: CreateUserModel) {
        const result = await this.authenticationService.registerUserAsync(createUserModel);

        // more code here
    }

    @Post('login')
    public async loginAsync(@Body() login: LoginModel): Promise<{ accessToken: string }> {
        const user = await this.usersService.getUserByUsernameAsync(login.username);

        // more code here
    }
}

Here is my unit test file:

describe('AuthController', () => {
    let authController: AuthController;
    let authService: AuthService;

    beforeEach(async () => {
        const moduleRef: TestingModule = await Test.createTestingModule({
            imports: [JwtModule],
            controllers: [AuthController],
            providers: [
                AuthService,
                UsersService,
                {
                    provide: getRepositoryToken(User),
                    useClass: Repository,
                },
            ],
        }).compile();

        authController = moduleRef.get<AuthenticationController>(AuthenticationController);
        authService = moduleRef.get<AuthenticationService>(AuthenticationService);
    });

    describe('registerAsync', () => {
        it('Returns registration status when user registration succeeds', async () => {
            let createUserModel: CreateUserModel = {...}

            let registrationStatus: RegistrationStatus = {
                success: true,
                message: 'User registered successfully',
            };

            jest.spyOn(authService, 'registerUserAsync').mockImplementation(() =>
                Promise.resolve(registrationStatus),
            );

            expect(await authController.registerAsync(createUserModel)).toEqual(registrationStatus);
        });
    });
});

But when running this, I get the following error(s):

  ● AuthController › registerAsync › Returns registration status when user registration succeeds

    Nest can't resolve dependencies of the JwtService (?). Please make sure that the argument JWT_MODULE_OPTIONS at index [0] is available in the JwtModule context.

    Potential solutions:
    - If JWT_MODULE_OPTIONS is a provider, is it part of the current JwtModule?
    - If JWT_MODULE_OPTIONS is exported from a separate @Module, is that module imported within JwtModule?
      @Module({
        imports: [ /* the Module containing JWT_MODULE_OPTIONS */ ]
      })

      at Injector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:191:19)
      at Injector.resolveComponentInstance (../node_modules/@nestjs/core/injector/injector.js:147:33)
      at resolveParam (../node_modules/@nestjs/core/injector/injector.js:101:38)
          at async Promise.all (index 0)
      at Injector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:116:27)
      at Injector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:80:9)
      at Injector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:37:9)
      at Injector.lookupComponentInImports (../node_modules/@nestjs/core/injector/injector.js:223:17)
      at Injector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:189:33)

  ● AuthController › registerAsync › Returns registration status when user registration succeeds

    Cannot spyOn on a primitive value; undefined given

      48 |             };
      49 |
    > 50 |             jest.spyOn(authService, 'registerUserAsync').mockImplementation(() =>
         |                  ^
      51 |                 Promise.resolve(registrationStatus),
      52 |             );
      53 |

      at ModuleMockerClass.spyOn (../node_modules/jest-mock/build/index.js:780:13)
      at Object.<anonymous> (Authentication/authentication.controller.spec.ts:50:18)

I'm not quite sure how to proceed so I'd like some help.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
noblerare
  • 10,277
  • 23
  • 78
  • 140

3 Answers3

4

There's a few things I'm noticing here:

  1. if you're testing the controller, you shouldn't need to mock more than one level deep pf services

  2. you should almost never have a use case where you need an imports array in a unit test.

What you can do for your test case instead is something similar to the following:

beforeEach(async () => {
  const modRef = await Test.createTestingModule({
    controllers: [AuthController],
    providers: [
      {
        provide: AuthService,
        useValue: {
          registerUserAsync: jest.fn(),
        }

      },
      {
        provide: UserService,
        useValue: {
          getUserByUsernameAsync: jest.fn(),
        }
      }
    ]
  }).compile();
});

Now you can get the auth service and user service using modRef.get() and save them to a variable to add mocks to them later. You can check out this testing repository which has a lot of other examples.

Jay McDoniel
  • 57,339
  • 7
  • 135
  • 147
1

Since you are registering AuthService in the dependency injection container and just spying on registerUserAsync, it requires JWTService to be registered as well.

You need to register dependencies that are injected in AuthService:

const moduleRef: TestingModule = await Test.createTestingModule({
  imports: [JwtModule],
  controllers: [AuthController],
  providers: [
    AuthService,
    UsersService,
    JWTService, // <--here
    {
      provide: getRepositoryToken(User),
      useClass: Repository,
    },
],
}).compile();

or register a fully mocked AuthService that doesn't need any other dependency:

const moduleRef: TestingModule = await Test.createTestingModule({
  imports: [JwtModule],
  controllers: [AuthController],
  providers: [
    {
      provide: AuthService,
      useValue: {
        registerUserAsync: jest.fn(), // <--here
      }
     },
    {
      provide: getRepositoryToken(User),
      useClass: Repository,
    },
],
}).compile();

  • I spent over an hour trying to figure out how to solve my problem with unit testing, and this was the only solution that worked for me. Thank you! – Dakota Cookenmaster Jul 13 '22 at 02:40
1

If you're building out a full integration test suite for NestJS then it will be easy to hit this error if you import a module that imports the AuthService. That will inevitably require the JwtService which will error out with: Nest can't resolve dependencies of the JwtService (?). Please make sure that the argument JWT_MODULE_OPTIONS at index [0] is available in the RootTestModule context.

Here's how I resolved this. I added:

                JwtModule.registerAsync({
                    imports: [ConfigModule],
                    inject: [ConfigService],
                    useFactory: async (configService: ConfigService) => ({
                        secret: configService.get('JWT_SECRET'),
                        signOptions: { expiresIn: '1d' }
                    })
                }),

To my imports: [] function inside my await Test.createTestingModule({ call

The final important thing to do was to not import JwtService directly. Instead, initialize JwtModule with the above code which by extension itself should internally initialize JwtService correctly.

fIwJlxSzApHEZIl
  • 11,861
  • 6
  • 62
  • 71