theKashey / rewiremock

The right way to mock dependencies in Node.js or webpack environment.
MIT License
490 stars 31 forks source link

Mock called, but not stubbing dependency #139

Open yuv-c opened 2 years ago

yuv-c commented 2 years ago

I am trying to test an express API. Upon calling one of the endpoints, the API calls the function loadFromFS. I'd like to mock its return value. The files are as follows:

// server.js:
const { router } = require('./api');

const app = express()
...
app.use('/api', authentication, authorization('@data/accounts'), router);
...
return app
//api.js:
const { searchSourceQueries } = require('../../../aliasing-utility/aliasing-helpers');
router.get('/sources',searchSourceQueries); <-- The endpoint i'm trying to test
// aliasing-helpers.js:
const { stuff } = require('stuff...)
...
const { loadFromFS } = require('@data/utils/queries-loader'); <-- the require i'm trying to mock

console.log('loading stuff with loadFromFS');
const customersQuery = loadFromFS(`${__dirname}/sql/sources/customers`);
// other constants initialized with loadFromFS
console.log(`Customers query: ${customersQuery}`);

const foo = () => {
  do SQL stuff with the constants...
// customers.sql:
SELECT 2;
// spec-prebuild.js:
const { expect } = require('chai');
const chai = require('chai');
const chaiHttp = require('chai-http');
const _ = require('lodash');
const rewiremock = require('rewiremock/node');
...

describe('Test some API', async () => {
  let serverFile;
  const = pushMockDataToDB () => {...}
  const = clearMockDataFromDB () => {...}

  before(async () => { // This works - mocking my auth middleware
    // eslint-disable-next-line global-require
    serverFile = rewiremock.proxy(() => require('../server/src/server'), {
      '@data/utils/server-auth-client': {
        authentication: authenticationMock,
        authorization: authorizationMock
      }
    });
    rewiremock.overrideEntryPoint(module);

    await clearMockDataFromDB();
    await pushMockDataToDB();
  });

  it('Should return all sources', async () => {

    const mockLoadFromFS = path => {
      console.log('Mock called with path:', path);
      return { foo: 'hello' };
    };

    rewiremock.proxy(
      () => require(`${getAliasingUtilityDirPath()}/aliasing-helpers`),
      {
        '@data/utils/queries-loader': {
          loadFromFS: mockLoadFromFS
        }
      }
    );

    const res = await chai.request(serverFile.app).get('/api/sources');
    expect(res).to.have.status(200); // An SQL syntax error was expected
    expect(_.last(res.body)[2]).to.equal(1);

Log:

loading stuff with loadFromFS
Customers query: {"customers":"select 2;"}
Mock data pushed to DB
loading stuff with loadFromFS
Mock called with path: /.../sql/sources/not-aliased
Mock called with path: /.../sql/sources/customers
Mock called with path: /.../sql/helpers
Customers query: {"foo":"hello"} <-- Mock succeeds
Received request for endpoint "sources"

AssertionError: expected 2 to equal 1 <-- Data is fetched, when it should fail and throw an error.
Expected :1
Actual   :2

As you can see, initially the variable customersQuery, is loaded as usual by , and then mocked. Even after it is mocked, foo gets the un-mocked version of it. If I move customersQuery to the inner scope of foo, the mock function is never called. Any help would be appreciated. Thank you.

Node version v14.15.4 rewiremock 3.14.3. mocha 9.1.3

yuv-c commented 2 years ago

I've re-written my use of rewiremock, and am able to get it to mock properly:

serverFile = rewiremock.proxy(() => require('../server/src/server'), {
      '@data/utils/server-auth-client': {
        authentication: authenticationMock,
        authorization: authorizationMock
      },
      '@data/utils/queries-loader': {
        loadFromFS: mockLoadFromFS
      }
    });
    rewiremock.overrideEntryPoint(module);

Notice that loadFromFS isn't directly required by server.js, so it was unclear i should be mocking it from there.

I do have another question - Is it possible to proxy(() => '../server/src/server'), {mockA} inside before function, and then mock other requires used by '../server/src/server' later during the test?

theKashey commented 1 year ago

Hey, sorry for quite a late answer, but it's better late that never?

History note - rewiremock supports different "models" of work in order to help migration from other libraries existing before it. rewiremock.proxy is mimicking proxyquire interface.

In short proxy(file, overrides) returns you a file with overrides 🤷‍♂️ overrides. Depending on different flags it can be direct children only, or any transitive dependency required by this file anywhere.

If you want to mocks something else in the same test you need to do const newServer = .proxy stuff again.


At the same time you can change rules a little bit and mock @data/utils/queries-loader instead of '../server/src/server'

rewiremock('@data/utils/queries-loader').with({loadFromFS: mockLoadFromFS});
const serverFile = require('../server/src/server');

In this you can do the following trick

const loaderMock = rewiremock('@data/utils/queries-loader')
  .callThrough() // pass to real file
  .dynamic() // allow changes in the future
const serverFile = require('../server/src/server');
// update mocking in the future
loaderMock.with({loadFromFS: mockLoadFromFS});

Keep in mind dynamic wraps module export in Proxy to be able to update it at runtime and some operations (assigning to a constant) can bypass it. So I cannot guarantee that it gonna work for every case.