unit-testingmockinges6-modulesnode-postgresvitest

How to mock node-postgres in vitest


I am trying to mock node-postgres in vitest with esm. But I am running into different problems. When I am using the Pool class in my function:

import {Pool} from 'pg';
...
const pool = new Pool({connectionString});
const client = await pool.connect();
await client.query('BEGIN');
...

When I am importing Pool as above with import {Pool} from 'pg' i can mock the pool with:

vi.mock('pg', () => {
    const Pool = vi.fn();
    const Client = vi.fn();
    return {Pool, Client};
});
function queryMockImplementation(migrationTableExists: boolean, migratedFiles?: string[]) {
    return (queryString: string) => {
        if (queryString === `SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'administration' AND tablename = 'migrations')`) {
            return Promise.resolve({rows: [{exists: migrationTableExists}]});
        }
        return Promise.resolve({rows: []});
    }
}
function connectMockImplementation(clientQueries: string[], throwOnSqlQuery?: string) {
    return async() => {
        return {
            query: vi.fn().mockImplementation((queryString: string) => {
                if (throwOnSqlQuery === queryString) {
                    throw new Error('Error thrown by mock for client query.');
                }
                clientQueries.push(queryString);
            }),
            release: vi.fn(),
        };
    };
}

Then in the test I can make the following:

it('Should mock', () => async {
            const {Pool} = await import('pg');
            const clientQueries: string[] = [];
            Pool.prototype.query = vi.fn().mockImplementation(queryMockImplementation(false));
            Pool.prototype.connect = vi.fn().mockImplementation(connectMockImplementation(clientQueries));
            const pool = new Pool();
}

But this is not the intended way. It works but I have to inject the array clientQueries into the mock implementation for the client to observe the strings called with client.query. I could not find a way to get the client directly mocked. Also this way of importing Pool creates problems with esm in packages consuming this library. So when I try to access Pool with:

import pg from 'pg';
const {Pool} = pg;

I get the vitest error:

Error: [vitest] No "default" export is defined on the "pg" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "vi.importActual" inside:

vi.mock("pg", async () => {
  const actual = await vi.importActual("pg")
  return {
    ...actual,
    // your mocked methods
  },
})

So I am adjusting the mock:

vi.mock('pg', async () => {
    const actual = <Record<string, any>>await vi.importActual("pg")
    const Pool = vi.fn();
    const Client = vi.fn();
    return {...actual, Pool, Client};
});

But now I have two other problems. I have to provide an actual database and connection string in the test and the client query mock is not working any more.


Solution

  • I got the issue fixed. Example repository at vitest-allowSyntheticDefaultImports

    import {describe, vi, it, expect} from 'vitest';
    import {connect} from './index';
    import pg from 'pg';
    const {Pool} = pg;
    
    vi.mock('pg', async () => {
        const actual = <Record<string, unknown>>await vi.importActual('pg');
    
        return {
            ...actual,
            default: {
                Pool: vi.fn(),
            }
        };
    });
    
    describe('connect', () => {
        it('should call Pool', () => {
            connect('postgres://...');
            expect(Pool).toBeCalledTimes(1);
        });
    });