unit-testingjestjsnestjsts-jest

How to test NestJS Controller if it has a lot of dependent services?


I have a trouble with Testing Nest JS Controller because I cannot realise how to make a Testing Module with all the dependencies. I've already tried Mocks but still it's not working. Here's how the controller I want to test looks like

calculator.controller.ts

@Controller('/calculator')
export class CalculatorController {
  constructor(
    @Inject(HISTORY_SERVICE)
    private historyService: HistoryService,
    @Inject(CACHE_SERVICE)
    private readonly cacheService: CacheService,
    @Inject(CALCULATOR_SERVICE)
    private readonly calculatorService: CalculatorService,
  ) {}
  @Get()
  getResult(@Query() expressionDto: ExpressionDto): Promise<ClientResponseDto> {
    const expression: string = expressionDto.expression;
    const response: Promise<ClientResponseDto> = this.cacheService
      .checkInCache(expression)
      .then((response) => {
        if (!response) {
          const calculationResult =
            this.calculatorService.getResult(expression);
          const clientDto = this.historyService
            .create({
              expression,
              result: calculationResult,
            })
            .then((dbResponse) => {
              return this.cacheService.setToCache(dbResponse);
            });
          return clientDto;
        }
        return this.historyService.create({ expression, result: response });
      });
    return response;
  }
}

And this is how it's spec looked like before mocks implementation

controller.spec.ts

let calculatorController: CalculatorController;
let calculatorService: CalculatorService;

beforeEach(async () => {
  const moduleRef = await Test.createTestingModule({
    imports: [HistoryModule],
    controllers: [CalculatorController],
    providers: [
      CalculatorService,
    ],
  })
    .useMocker(() => createMock())
    .compile();
  calculatorController =
    moduleRef.get<CalculatorController>(CalculatorController);
  calculatorService = moduleRef.get<CalculatorService>(CalculatorService);
  jest.clearAllMocks();
});

describe('Calculator Controller', () => {
  it('should be defined', () => {
    expect(calculatorController).toBeDefined();
  });
  it('should have all methods', () => {
    expect(calculatorController.getResult).toBeDefined();
    expect(calculatorController.getResult(calculatorStub().request)).toBe(
      typeof Promise,
    );
  });
});

And this test failed when calling getResult function cause inside this Function firstly I call CacheService to check data in Cache. So at this moment test failed telling that

TypeError: this.cacheService.checkInCache(...).then is not a function

      24 |     const response: Promise<ClientResponseDto> = this.cacheService
      25 |       .checkInCache(expression)
    > 26 |       .then((response) => {
         |        ^

I started to think that the problem is Testing module somehow doesn't have access to the Cache Service, so I added mock to the providers like this

let calculatorController: CalculatorController;
let calculatorService: CalculatorService;

beforeEach(async () => {
  const moduleRef = await Test.createTestingModule({
    imports: [HistoryModule],
    controllers: [CalculatorController],
    providers: [
      CalculatorService,
      {
        provide: CacheService,
        useValue: {
          checkInCache: jest.fn().mockResolvedValue(Promise<null>),
        },
      },
    ],
  })
    .useMocker(() => createMock())
    .compile();
  calculatorController =
    moduleRef.get<CalculatorController>(CalculatorController);
  calculatorService = moduleRef.get<CalculatorService>(CalculatorService);
  jest.clearAllMocks();
});

But now tests don't even run cause I have Nest dependencies problems

Nest can't resolve dependencies of the CalculatorController (HISTORY_SERVICE, ?,

CALCULATOR_SERVICE). Please make sure that the argument dependency at index [1] is 

available in the RootTestModule context.

What is the issue and how is it possible to solve this problem?


Solution

  • Generally speaking, when unit testing a service or a controller, you want to provide mocks for the controller's or service's dependencies. Most of the time, these are going to be objects with the same method names but the methods are set to be jest.fn() or similar for other mock libraries. You'll want to use custom providers to create the mock providers that will be injected. Taking your controller above, you'll want the setup of your test to look something like this:

    describe('CaclulatorController', () => {
      let controller: CalculatorController;
      let service: Pick<jest.MockedObject<CalculatorService>, 'getResult'>;
      let cache: Pick<jest.MockedObject<CacheService>, 'checkInCache' | 'setToCache'>;
      let history: Pick<jest.MockedObject<HistoryService>, 'create'>;
    
      beforeAll(async () => {
        const modRef = await Test.createTestModule({
          controller: [CalculatorController],
          providers: [
            {
              provide: CALCULATOR_SERVICE,
              useValue: {
                getResult: jest.fn(),
              },
            },
            {
              provide: CACHE_SERVICE,
              useValue: {
                checkInCache: jest.fn(),
                setToCache: jest.fn(),
              },
            },
            {
              provide: HISTORY_SERVICE,
              useValue: {
                create: jest.fn(),
              },
            },
          ]
        }).compile();
    
        controller = modRef.get(CalculatorController);
        service = modRef.get(CALCULATOR_SERVICE);
        cache = modRef.get(CACHE_SERVICE);
        history = modRef.get(HISTORY_SERVICE);
      });
    

    Okay that's a lot to look at at once, so let's step through the big parts and explain what's going on here. The first this I do is set up local variables to reference during the test for the class that I'm testing (CalculatorController) and the dependencies of the class so I can modify them as necessary. Next, I use a Pick<T, K> generic with the jest.MockedOject<T> generic to tell Typescript that "This class has been mocked by jest, and I only am worried about these methods of it" so later on when I use cache. I'll get some intellisense for the checkInCache and setToCache methods, and they'll have jest's mock function types.

    In the beforeAll I set up the initial mocks for the dependencies, you can also set return values here using the appropriate mockReturnValue or mockResolvedValue methods.

    Now that the mocks and dependencies are set up, we can actually write a test. My approach is to use a describe block per method and its per variation of the method's outcome and branches. I'll write a single branch to show you and let you work out the rest from there.

    
      describe('getResult', () => {
        it('should get no response from the cache and perform a full caclulation', async () => {
          cache.checkInCache.mockResolvedValueOnce(undefined);
          service.getResult.mockResolvedValueOnce(calculationResult);
          histoy.create.mockResolvedValueOnce(dbResult);
          cache.setInCache.mockResolvedValueOnce(cacheSaveResult);
          await expect(controller.getResult({ expression: someExpression })).resolves.toEqual(cacheSaveResult)
        });
      })
    

    This should cover the case where there's no value in the cache and the full set of steps has to be taken. By using mockResolvedValueOnce we ensure that the methods don't return if called more than once as that's most likely not the expected case here, and we're making sure to return promsies as you use .thens. You might want to look into async/await syntax to help clean that up.

    putting the two snippets together we have the following:

    describe('CaclulatorController', () => {
      let controller: CalculatorController;
      let service: Pick<jest.MockedObject<CalculatorService>, 'getResult'>;
      let cache: Pick<jest.MockedObject<CacheService>, 'checkInCache' | 'setToCache'>;
      let history: Pick<jest.MockedObject<HistoryService>, 'create'>;
    
      beforeAll(async () => {
        const modRef = await Test.createTestModule({
          controller: [CalculatorController],
          providers: [
            {
              provide: CALCULATOR_SERVICE,
              useValue: {
                getResult: jest.fn(),
              },
            },
            {
              provide: CACHE_SERVICE,
              useValue: {
                checkInCache: jest.fn(),
                setToCache: jest.fn(),
              },
            },
            {
              provide: HISTORY_SERVICE,
              useValue: {
                create: jest.fn(),
              },
            },
          ]
        }).compile();
    
        controller = modRef.get(CalculatorController);
        service = modRef.get(CALCULATOR_SERVICE);
        cache = modRef.get(CACHE_SERVICE);
        history = modRef.get(HISTORY_SERVICE);
      });
    
      describe('getResult', () => {
        it('should get no response from the cache and perform a full caclulation', async () => {
          cache.checkInCache.mockResolvedValueOnce(undefined);
          service.getResult.mockResolvedValueOnce(calculationResult);
          histoy.create.mockResolvedValueOnce(dbResult);
          cache.setInCache.mockResolvedValueOnce(cacheSaveResult);
          await expect(controller.getResult({ expression: someExpression })).resolves.toEqual(cacheSaveResult)
        });
      });
    });
    

    That should be enough to get you started on testing the rest of your controller. If you need more test setup examples, there's an entire GitHub repository of them with different setups