1

I have a Jest test suite of integration tests using a suite-global mock for database access, where depending on the SQL I return different mock responses:

jest.mock('@my-org/our-mysql-wrapper', () => {
    const query = jest.fn(async (sql, params) => {
        if (sql === 'select foo from bar') {
            return [];
        } else if (sql === 'select baz') {
            return [{ messageId: 3 }, { messageId: 4 }, { messageId: 5 }];
        } else if (...) {
            return ...;
        } else {
            console.log('UNEXPECTED QUERY SENT TO MOCK: ', sql, params);
            return [];
        }
    });
    const end = jest.fn(async () => true);

    return jest.fn(() => ({ query, end }));
});

describe('suite', () => {
    //tests here
});

This works great for positive tests, but where I'm getting frustrated with it is for negative tests. For example, in some cases if the DB were to return no results we might want to throw an error. In order to test that I need to have my mock behave differently for the same input. Whereas a typical positive test would not overwrite the db mock before running, the negative test needs to:

it('should throw and handle an error if the db returns no results for Widget lookup', async () => {
    const mockDB = require('@my-org/our-mysql-wrapper')();
    mockDB.query.mockImplementation(jest.fn(asnyc (sql, params) => {
        if ( sql === 'select * from Widgets' ){
            //this is the use-case that I want to override for this test
            return [];
        }else{
            //...
        }
    }));
    
    const someValue = await tool.doThing();
    expect(buglogger).toHaveBeenCalled(); //actual test will be more specific...

    //I tried plugging in mockRestore/mockClear/mockReset here
});

As written above this test actually passes, but it breaks tests that run after it because it doesn't clean up after itself. To the best of my understanding, this is what mockClear(), mockReset(), and mockRestore() are supposed to do in different variations; but I haven't been able to find a way to restore my mock to the original pre-override mocked implementation at the end of my test.

I've also used jest.spyOn() in some other cases, but that doesn't do what I'm after either. In this case, my test fails and the mock remains broken for other tests, too.

it('should throw and handle an error if the db returns no results for Widget lookup', async () => {
    const mockDB = require('@my-org/our-mysql-wrapper')();
    jest.spyOn(mockDb, 'query');
    mockDB.query.mockImplementation(jest.fn(asnyc (sql, params) => {
        if ( sql === 'select * from Widgets' ){
            //this is the use-case that I want to override for this test
            return [];
        }else{
            //...
        }
    }));
    
    const someValue = await tool.doThing();
    expect(buglogger).toHaveBeenCalled(); //actual test will be more specific...
    mockDb.query.mockRestore();
});

I have also tried mockImplementationOnce() and that won't work for me because the query in question is not the first one that will be run. Using this method does clean up after itself automatically, but doesn't (can't, as far as I can tell) make my test pass because it cleans itself up after the first use before the query in question is called.

BUT since mockImplementationOnce can clean itself up in a way that restores the original mock, shouldn't there be some manual way to override an existing mock just for one test? That's what mockImplementationOnce is doing, isn't it? (cleaning up after first call not after 1 test; but it appears to be restoring the original mock...)

What am I doing wrong, here?

Adam Tuttle
  • 19,505
  • 17
  • 80
  • 113
  • 1
    I'm not 100% sure, but i think your understanding of mockClear et al is wrong... it'll reset the mock to the original function e.g. original returns 42, mock returns 52, mockclear will make it start returning 42 after it's called. I think you need to look at beforeEach and setup the mocked function there, so that before each test (it), it'll mock the function, then on your failure you override the mock as you have done, then the next test will reset to the default mock setup – Jarede Jun 04 '21 at 15:31
  • @Jarede I think I do understand what those methods are doing, but I was hoping they would sort of stack push/pop rather than a full reset. The beforeEach approach did cross my mind. I'm doing something similar with fetch. longer term I was hoping to refactor the tests to something that doesn't have to redefine the entire function, only the case that the test wants to override. I may not be able to get around it, though. – Adam Tuttle Jun 04 '21 at 18:59

1 Answers1

0

I think the problem is in both the scenarios, you're trying to assign the mock to the actual package you imported.

Did you try to change your mock setup like below:

const mockDB = require('@my-org/our-mysql-wrapper')();
const dbSpy = jest.spyOn(mockDB, 'query');

dbSpy.mockImplementation(jest.fn(asnyc (sql, params) => {
  ...
}));

dbSpy.mockClear();
dbSpy.mockRestore();
Eranga Heshan
  • 5,133
  • 4
  • 25
  • 48
  • That does seem to be a valid thing to do, but it's still affecting other tests. – Adam Tuttle Jun 04 '21 at 18:22
  • Actually, I didn't try exactly what you suggested. I still have -- and want to keep -- my jest.mock() call for the default/shared mock between tests (so in your example, it would be between lines 1 and 2). I've tried with your creation of the spy as both a global and inside the test. either way, it affects other tests. – Adam Tuttle Jun 04 '21 at 18:26
  • Can you share how you export your `@my-org/our-mysql-wrapper` module? – Eranga Heshan Jun 05 '21 at 01:39
  • the wrapper module is a factory function that accepts a customer and environment as input (e.g. FooCompany, QA) and returns a mysql connection pool after it has connected to the db. This is simplified to fit but shows the basics: `module.exports = (cust, env) => { settings = getCustomerSettings(cust,env); return mysql.createPool({ ...settings }); }` – Adam Tuttle Jun 05 '21 at 13:08
  • @AdamTuttle Did you solve this ? – Amir Choubani Sep 20 '21 at 14:14
  • I had completely forgotten about this post, had to set that project aside, and came back to it this week. I've changed my approach a little bit and ended up with this question and answer: https://stackoverflow.com/questions/69531158/jest-mocks-bleeding-between-tests-reset-isnt-fixing-it -- not exactly the same problem code, but the new approach is working for all of my tests. – Adam Tuttle Oct 13 '21 at 20:16