8

Given an Injectable that uses a queue via the @InjectQueue decorator:

@Injectable()
export class EnqueuerService {
  constructor (
    @InjectQueue(QUEUE_NAME) private readonly queue: Queue
  ) {
  }

  async foo () {
    return this.queue.add('job')
  }
}

How can I test that this service calls the queue correctly? I can do the bsic scaffolding:

describe('EnqueuerService', () => {
  let module: TestingModule
  let enqueuerService: EnqueuerService

  beforeAll(async () => {
    module = await Test.createTestingModule({
      imports: [EnqueuerModule]
    }).compile()
    enqueuerService = module.get(EnqueuerService)

    // Here I'd usually pull in the dependency to test against:
    // queue = module.get(QUEUE_NAME)
    //
    // (but this doesn't work because queue is using the @InjectQueue decorator)
  })

  afterAll(async () => await module.close())

  describe('#foo', () => {
    it('adds a job', async () => {
      await enqueuerService.foo()

      // Something like this would be nice: 
      // expect(queue.add).toBeCalledTimes(1)
      //
      // (but maybe there are alternative ways that are easier?)
    })
  })
})

I'm quite lost in the Nest DI container setup but I suspect there's some clever way of doing this. But despite hours of attempts I can't make progress, and the documentation isn't helping me. Can anyone offer a solution? It doesn't have to be mocking, if it's easier to create a real queue to test against that's fine too I just want to verify my service enqueues as expected! Any help appreciated.

Lin Du
  • 88,126
  • 95
  • 281
  • 483
Jon Lauridsen
  • 2,521
  • 5
  • 31
  • 38

1 Answers1

16

By looking into the lib, I found a helper function that creates the queue injection token based on its name: getQueueToken(name?: string).

The function is exported from the lib so you can use it to provide you own queue implementation for testing.

import { getQueueToken } from '@nestjs/bull';

describe('EnqueuerService', () => {
  let module: TestingModule
  let enqueuerService: EnqueuerService

  beforeAll(async () => {
    module = await Test.createTestingModule({
      imports: [EnqueuerModule]
    })
      .overrideProvider(getQueueToken(QUEUE_NAME))
      .useValue({ /* mocked queue */ })
      .compile()

    enqueuerService = module.get(EnqueuerService)
    queue = module.get(QUEUE_NAME)
  })

  afterAll(async () => await module.close())

  describe('#foo', () => {
    it('adds a job', async () => {
      await enqueuerService.foo()

      expect(queue.add).toBeCalledTimes(1)
    })
  })
})

EDIT

To check the methods called of the queue service, you can create a mock at the root of your test file. I reorganized the test file to have a cleaner speration between the setup of the test and the test itself:

let module: TestingModule
let enqueuerService: EnqueuerService
let mockQueue;

// Create a helper function to create the app.
const createApp = async () => {
  module = await Test.createTestingModule({
    imports: [EnqueuerModule]
  })
    .overrideProvider(getQueueToken(QUEUE_NAME))
    .useValue(mockQueue)
    .compile()

  enqueuerService = module.get(EnqueuerService)
}

describe('EnqueuerService', () => {    
  // Recreate app before each test to clean the mock.
  beforeEach(async () => {
    mockQueue = {
      add: jest.fn(),
    };
    await createApp();
  })

  afterAll(async () => await module.close())

  describe('#foo', () => {
    it('adds a job', async () => {
      await enqueuerService.foo()

      // Check calls on the mock.
      expect(mockQueue.add).toBeCalledTimes(1)
    })
  })

  describe('#bar', () => {
    // Override the mock for a specific test suite.
    beforeEach(async () => {
      mockQueue.add = jest.fn().mockImplementation(/** */);
      // The mock has changed so we need to recreate the app to use the new value.
      await createApp();
    })

    it('adds a job', async () => {
      // ...
    })
  })
})
carpamon
  • 6,515
  • 3
  • 38
  • 51
Baboo
  • 4,008
  • 3
  • 18
  • 33
  • That looks promising! I get this error though: `Error: Nest could not find queue element`, it comes from the `module.get(QUEUE_NAME)` line. FWIW if I remove the "queue" references entirely the test runs (albeit with no assertions), so there is **something** here that works, it feels very close to working fully. – Jon Lauridsen Jul 06 '21 at 08:55
  • I added an example on how to mock the queue without relying on nest to retrieve the mock – Baboo Jul 06 '21 at 10:15
  • Very nice, thanks. FWIW I ended up using [jest-mock-extended](https://github.com/marchaos/jest-mock-extended) to create the mock (`mockQueue = mock();`). – Jon Lauridsen Jul 07 '21 at 13:24