8

Hi on a test suite it appears to me that I have 2 living instances of a same provider, one for the implementation and another one for the real implementation.

I base my conclusion in a fact that on my test I tried replace a method by a jest.fn call but still, on the service I am testing the method still points to the original implementation.

What makes it even more odd is that I was able to mock another service performing exactly the same procedure, as if, depending on how those services were injected (where they came from in the container graph) it would or not work.

I'll try to share some snippets, but of course, only a small repo could actually reproduce it, but perhaps someone has an insight:

 beforeAll(async done => {
    app = await Test.createTestingModule({
      imports: [
        SOME_MODULES,
      ],
      providers: [
        EssayApplicationService,
        ReviewFacade,
        ExamCacheResultService,
      ],
    }).compile();

    essayApplicationService = app.get<EssayApplicationService>(EssayApplicationService)
    reviewFacade = app.get<ReviewFacade>(ReviewFacade)
    examCacheResult = app.get<ExamCacheResultService>(ExamCacheResultService)
    await app.init()
    done()
  })
it('should invoke review only once', async done => {

    reviewFacade.startReview = jest.fn() --> this works
    examCacheResult.clearCachedResult = jest.fn() --> this fails

    await essayApplicationService.finishApplication()

    expect(reviewFacade.startReview).toHaveBeenCalledTimes(1)
    expect(reviewFacade.startReview).toHaveBeenCalledWith(expect.objectContaining({ id: 1 }))
    expect(examCacheResult.clearCachedResult).toHaveBeenCalledTimes(1) ---> here this fails, although it's called!!

So,the issue boils down to the fact that I'm 100% positive that both methods were called on the service under test, but the second for some reason wasn't replaced by the mock

Kim Kern
  • 54,283
  • 17
  • 197
  • 195

1 Answers1

15

You are mixing the concepts of unit tests and end to end (e2e) tests. You are importing a module and at the same time you import single providers directly. I'm assuming that one of your imported modules also imports ExamCacheResultService. This way, you have two of them in your test application. When you call app.get(ExamCacheResultService), you will get the instance that is directly declared in your testing module. But the one that is used when you call finishApplication is the other one. Decide what you want to test and follow the following principles:

Unit test

In a unit test you want to test a single provider/controller isolated from other dependencies, e.g. UserService or UsersController. You import this provider and its injected dependencies as mocks. You do not import a module.

Let's assume we have a UsersService that depends on a DatabaseConnection:

export class UsersService {
  constructor(private connection: DatabaseConnection) {}
  // ...
}

In your unit test, you import the UsersService, you mock the DatabaseConnection but you do not import the UsersModule.

module = await Test.createTestingModule({
  providers: [
    UsersService,
    { provide: DatabaseConnection, useClass: DbConnectionMock },
  ],
}).compile();
databaseMock = module.get(DatabaseConnection);
databaseMock.findMany.mockReturnValue([]);

E2E test

In an end to end test, you want to test your whole application and therewith the interaction between the parts that you have unit tested beforehand. So you do not import single providers but instead a module, typically the AppModule. You can then override single providers, e.g. if you want to test on an in-memory database instead of an actual one or you want to mock the results of an external API.

const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    }).overrideProvider(DatabaseConnection).useClass(InMemoryDatabaseConnection)
      .overrideProvider(ExternalApiService).useValue(externalApiMock)
      .compile();
    app = moduleFixture.createNestApplication();
    externalApiMock.get.mockReturnValueOnce({data: [...]});
    await app.init();

How to create mocks?

See this answer.

Kim Kern
  • 54,283
  • 17
  • 197
  • 195
  • Although it doesn't really solves the question is a very useful piece of information! – Luiz Henrique Martins Lins Rol Mar 15 '19 at 16:11
  • 1
    I had imported the providers due to another issue I had with some unclear unresolved dependencies. After restructuring the app a bit to remove some "cycles" I was able to setup in a very similar fashion to the e2e (i.e only the modules). There are, though, some very obscure error messages for some dependency resolution scenarios, and the worst, only for tests. But nonetheless, still, nest rocks after one learns some tweaks – Luiz Henrique Martins Lins Rol Mar 15 '19 at 16:15
  • This did lead me to realize I had specified my dependency in two places: once in the `providers` of the module where it was defined, and a second time in the `providers` of another module consuming the first, making Nest resolve two separate instances. The solution was to indicate the dependency as `exports` on the first module, and avoid specifying it in `providers` of the consuming module. Basically, **if you want a single instance of something, make sure it's only listed in a single module's `providers` list** (for your entire app). – Paul Jul 27 '22 at 00:03
  • 1
    @Paul Good find! There is a dedicated thread about exporting providers, see here: https://stackoverflow.com/a/51821523/4694994 – Kim Kern Jul 27 '22 at 11:13