Search code examples
typescriptunit-testingtestingsequelize.jsnestjs

Nest.js Tests Can't resolve dependencies of ReportingService (?, MasterMLRepository) - Sequelize is not available in the RootTestModule context


I'm writing tests in Nest.js. And I came across this error. I tried all the available solutions on the internet. Adding Sequelize to imports, exports, etc. But, none of them worked.

 Nest can't resolve dependencies of the ReportingService (?, MasterMLRepository). Please make sure that the argument Sequelize at index [0] is available in the RootTestModule context.

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

Please refer to the files below:

reporting.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { PinoLogger } from 'nestjs-pino';
import { MasterML } from './reporting.model';
import { ReportingService } from './reporting.service';

@Controller()
export class ReportingController {
  constructor(
    private readonly reportingService: ReportingService,
    private readonly logger: PinoLogger,
  ) {
    this.logger.setContext(ReportingController.name);
  }

  @Get()
  async getMaterialIds(
    @Param('plantId') plantId: string,
  ): Promise<string[] | []> {
    try {
      return await this.reportingService.getMaterialIds(plantId);
    } catch (error) {
      this.logger.error(error);
      throw error;
    }
  }
}

reporting.model.ts

import { Column, Model, Table } from 'sequelize-typescript';

@Table({ tableName: 'MASTER_ML' })
export class MasterML extends Model {
  @Column({ field: 'BATCH_ID' })
  batchId: string;

  @Column({ field: 'FEATURE_NAME' })
  featureName: string;

  @Column({ field: 'FEATURE_VALUE' })
  featureValue: string;

  @Column({ field: 'MATERIAL_ID' })
  materialId: string;

  @Column({ field: 'PLANT_ID' })
  plantId: string;

  @Column({ field: 'STAGE' })
  stage: string;

  @Column({ field: 'MANUFACTURE_DATE' })
  manufactureDate: Date;
}

reporting.module.ts

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { Sequelize } from 'sequelize-typescript';
import { ReportingController } from './reporting.controller';
import { ReportingService } from './reporting.service';
import { MasterML } from './reporting.model';

@Module({
  imports: [SequelizeModule.forFeature([
    MasterML,
  ])],
  controllers: [ReportingController],
  providers: [ReportingService],
})
export class ReportingModule {}

reporting.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Sequelize } from 'sequelize-typescript';
import { MasterML } from './reporting.model';

@Injectable()
export class ReportingService {
  materialIdAttributes: any[] = [
    [
      this.sequelize.fn('DISTINCT', this.sequelize.col('MATERIAL_ID')),
      'materialId',
    ],
  ];

  materialIdOrder: any[] = [[this.sequelize.literal("'materialId' ASC")]];

  constructor(
    public sequelize: Sequelize,
    @InjectModel(MasterML) private readonly masterMl: typeof MasterML,
  ) {}

  async getMaterialIds(
    plantId: string,
  ): Promise<string[] | []> {
    return this.masterMl.findAll({
      attributes: this.materialIdAttributes,
      where: { plantId },
      order: this.materialIdOrder,
    }).then((rows) => rows.map((row) => row.materialId));
  }
}

reporting.service.spec.ts

import { getModelToken } from '@nestjs/sequelize';
import { Test } from '@nestjs/testing';
import { MasterML } from '../../../src/simply/reporting/reporting.model';
import { ReportingService } from '../../../src/simply/reporting/reporting.service';

const testMaterialIds: string[] = [
  'MTRL-1',
  'MTRL-2',
  'MTRL-3',
];

describe('ReportingService', () => {
  let service: ReportingService;
  let masterMlModel: typeof MasterML;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        ReportingService,
        {
          provide: getModelToken(MasterML),
          useValue: {
            findAll: jest.fn(() => [testMaterialIds]),
          },
        },
      ],
    }).compile();
    service = module.get(ReportingService);
    masterMlModel = module.get<typeof MasterML>(getModelToken(MasterML));
  });

  it('Service should be defined', () => {
    expect(service).toBeDefined();
  });

  it('getMaterialIds - should be defined', async () => {
    expect(await service.getMaterialIds('PLNT1')).toBeDefined();
  });

  it('should get the getMaterialIds', async () => {
    expect(await service.getMaterialIds('PLNT1')).toEqual([testMaterialIds]);
    const findAllSpy = jest.spyOn(masterMlModel, 'findAll');
    expect(findAllSpy).toHaveBeenCalled();
  });
});

simply.module.ts

import { Module } from '@nestjs/common';
import { Routes, RouterModule } from '@nestjs/core';
import { Sequelize } from 'sequelize-typescript';
import { PlantModule } from './plant/plant.module';
import { Plant } from './plant/plant.model';
import { ProductModule } from './product/product.module';
import { ProcessModule } from './process/process.module';
import { ReportingModule } from './reporting/reporting.module';
import { Product } from './product/product.model';
import { Process } from './process/process.model';
import { MasterML } from './reporting/reporting.model';

const routes: Routes = [
  {
    path: '/plants',
    module: PlantModule,
    children: [
      {
        path: '/:plantId/products',
        module: ProductModule,
        children: [
          {
            path: '/:productId/processes',
            module: ProcessModule,
          },
        ],
      },
      {
        path: '/:plantId/reporting',
        children: [
          {
            path: '/materialids',
            module: ReportingModule,
          },
        ],
      },
    ],
  },
];
@Module({
  imports: [
    RouterModule.register(routes),
    PlantModule,
    ProductModule,
    ProcessModule,
    ReportingModule,
  ],
})
export class SimplyModule {
  constructor(public sequelize: Sequelize) {
    this.sequelize.addModels([
      Plant,
      Product,
      Process,
      MasterML,
    ]);
  }
}

app.module.ts

import { Module } from '@nestjs/common';
import { LoggerModule, Params } from 'nestjs-pino';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TerminusModule } from '@nestjs/terminus';
import { SequelizeModuleOptions, SequelizeModule } from '@nestjs/sequelize';
import {
  apiConfig, logConfig, sequelizeConfig,
} from './config';

import JoiSchema from './config/schema/validation.schema';
import { AuthModule } from './auth/auth.module';
import { TerminusController } from './terminus/terminus.controller';
import { SimplyModule } from './simply/simply.module';

const imports = [
  TerminusModule,
  ConfigModule.forRoot({
    validationSchema: JoiSchema,
    validationOptions: {
      allowUnknown: true,
      abortEarly: false,
    },
    isGlobal: true,
    envFilePath: process.env.NODE_ENV ? `env/.env.${process.env.NODE_ENV}` : 'env/.env.local',
    load: [
      apiConfig,
      logConfig,
      sequelizeConfig,
    ],
  }),

  LoggerModule.forRootAsync({
    useFactory: async (configService: ConfigService) => configService.get<Params>('log'),
    inject: [ConfigService],
  }),
  SequelizeModule.forRootAsync({
    useFactory: (configService: ConfigService) =>
      configService.get<SequelizeModuleOptions>('sequelize'),
    inject: [ConfigService],
  }),
  AuthModule,
  SimplyModule,
];

@Module({
  imports,
  controllers: [TerminusController],
  providers: [],
})
export class AppModule { }

Solution

  • I have mocked Sequelize and its functions -

    beforeEach(async () => {
        const module = await Test.createTestingModule({
          providers: [
            ProcessReportingService,
            {
              provide: Sequelize,
              useValue: {
                fn: jest.fn,
                col: jest.fn,
                literal: jest.fn,
                where: jest.fn,
              },
            },
            {
              provide: getModelToken(MasterML),
              useValue: {
                findAll: jest.fn(() => new Promise((resolve) => {
                  resolve(testRawMaterialIds);
                })),
              },
            },
            {
              provide: getModelToken(Int2Calculations),
              useValue: {
                findAll: jest.fn(() => new Promise((resolve) => {
                  resolve(testInt2CalculationsRows);
                })),
              },
            },
          ],
        }).compile();
        service = module.get(ProcessReportingService);
        masterMlModel = module.get<typeof MasterML>(getModelToken(MasterML));
        int2CalculationsModel = module.get<typeof Int2Calculations>(getModelToken(Int2Calculations));
      });