Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the proper way to unit test Service with NestJS/Elastic

Im trying to unit test a Service that uses elastic search. I want to make sure I am using the right techniques.

I am new user to many areas of this problem, so most of my attempts have been from reading other problems similar to this and trying out the ones that make sense in my use case. I believe I am missing a field within the createTestingModule. Also sometimes I see providers: [Service] and others components: [Service].

   const module: TestingModule = await Test.createTestingModule({
      providers: [PoolJobService],
    }).compile()

This is the current error I have:

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

Here is my code:

PoolJobService

import { Injectable } from '@nestjs/common'
import { ElasticSearchService } from '../ElasticSearch/ElasticSearchService'

@Injectable()
export class PoolJobService {
  constructor(private readonly esService: ElasticSearchService) {}

  async getPoolJobs() {
    return this.esService.getElasticSearchData('pool/job')
  }
}

PoolJobService.spec.ts

import { Test, TestingModule } from '@nestjs/testing'
import { PoolJobService } from './PoolJobService'

describe('PoolJobService', () => {
  let poolJobService: PoolJobService

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [PoolJobService],
    }).compile()

    poolJobService = module.get<PoolJobService>(PoolJobService)
  })

  it('should be defined', () => {
    expect(poolJobService).toBeDefined()
  })

I could also use some insight on this, but haven't been able to properly test this because of the current issue

  it('should return all PoolJobs', async () => {
    jest
      .spyOn(poolJobService, 'getPoolJobs')
      .mockImplementation(() => Promise.resolve([]))

    expect(await poolJobService.getPoolJobs()).resolves.toEqual([])
  })
})

like image 338
Chang Yea Moon Avatar asked Jun 26 '19 22:06

Chang Yea Moon


People also ask

What is unit testing in NestJS?

Unit test cases in any application can detect bugs early, improve the quality of code, simplify the debugging process and reduce cost. Let's say we have the below files in a NestJS module named CatModule and I will demonstrate how to write unit test case for our file cats.

How do you run a unit test?

To run all the tests in a default group, choose the Run icon and then choose the group on the menu. Select the individual tests that you want to run, open the right-click menu for a selected test and then choose Run Selected Tests (or press Ctrl + R, T).

What are unit test scripts?

The unit test scripts are generally run as a part of continuous integration (CI) where the build is fired and, after the build is generated, the unit test scripts are run against the assemblies.


1 Answers

First off, you're correct about using providers. Components is an Angular specific thing that does not exist in Nest. The closest thing we have are controllers.

What you should be doing for a unit test is testing what the return of a single function is without digging deeper into the code base itself. In the example you've provided you would want to mock out your ElasticSearchServices with a jest.mock and assert the return of the PoolJobService method.

Nest provides a very nice way for us to do this with Test.createTestingModule as you've already pointed out. Your solution would look similar to the following:

PoolJobService.spec.ts

import { Test, TestingModule } from '@nestjs/testing'
import { PoolJobService } from './PoolJobService'
import { ElasticSearchService } from '../ElasticSearch/ElasticSearchService'

describe('PoolJobService', () => {
  let poolJobService: PoolJobService
  let elasticService: ElasticSearchService // this line is optional, but I find it useful when overriding mocking functionality

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PoolJobService,
        {
          provide: ElasticSearchService,
          useValue: {
            getElasticSearchData: jest.fn()
          }
        }
      ],
    }).compile()

    poolJobService = module.get<PoolJobService>(PoolJobService)
    elasticService = module.get<ElasticSearchService>(ElasticSearchService)
  })

  it('should be defined', () => {
    expect(poolJobService).toBeDefined()
  })
  it('should give the expected return', async () => {
    elasticService.getElasticSearchData = jest.fn().mockReturnValue({data: 'your object here'})
    const poolJobs = await poolJobService.getPoolJobs()
    expect(poolJobs).toEqual({data: 'your object here'})
  })

You could achieve the same functionality with a jest.spy instead of a mock, but that is up to you on how you want to implement the functionality.

As a basic rule, whatever is in your constructor, you will need to mock it, and as long as you mock it, whatever is in the mocked object's constructor can be ignored. Happy testing!

EDIT 6/27/2019

About why we mock ElasticSearchService: A unit test is designed to test a specific segment of code and not make interactions with code outside of the tested function. In this case, we are testing the function getPoolJobs of the PoolJobService class. This means that we don't really need to go all out and connect to a database or external server as this could make our tests slow/prone to breaking if the server is down/modify data we don't want to modify. Instead, we mock out the external dependencies (ElasticSearchService) to return a value that we can control (in theory this will look very similar to real data, but for the context of this question I made it a string). Then we test that getPoolJobs returns the value that ElasticSearchService's getElasticSearchData function returns, as that is the functionality of this function.

This seems rather trivial in this case and may not seem useful, but when there starts to be business logic after the external call then it becomes clear why we would want to mock. Say that we have some sort of data transformation to make the string uppercase before we return from the getPoolJobs method

export class PoolJobService {

  constructor(private readonly elasticSearchService: ElasticSearchService) {}

  getPoolJobs(data: any): string {
    const returnData = this.elasticSearchService.getElasticSearchData(data);
    return returnData.toUpperCase();
  }
}

From here in the test we can tell getElasticSearchData what to return and easily assert that getPoolJobs does it's necessary logic (asserting that the string really is upperCased) without worrying about the logic inside getElasticSearchData or about making any network calls. For a function that does nothing but return another functions output, it does feel a little bit like cheating on your tests, but in reality you aren't. You're following the testing patterns used by most others in the community.

When you move on to integration and e2e tests, then you'll want to have your external callouts and make sure that your search query is returning what you expect, but that is outside the scope of unit testing.

like image 115
Jay McDoniel Avatar answered Oct 09 '22 10:10

Jay McDoniel