Search code examples
typescriptsqliteunit-testingjestjsmocking

Mock sqlite3 Using Jest


I' struggling mocking my sqlite3 implementation for my jest tests. I prepared a Stackblitz with my problem, that I unfortunately do not get to run jest with :(

I want to write tests for the attach method and basically just check whether db.prepare and db.prepare.run have been called with correct arguments:

async attach(id: string): Promise<void> {
    await this.dbService.query((cb) => this.dbService.db.prepare(
      "UPDATE table SET boolean = true WHERE id = ?",
      id, cb).run()
    );
  }

sqlite3 implementation in my db service:

import sqlite3 from "sqlite3";

export class DBService {

  db: sqlite3.Database;

  private static instance: DBService | undefined = undefined;

  constructor() {
    this.db = new sqlite3.Database(`${someFolder}/somedb.db`);
  }

  public static getInstance(): DBService {
    if (!this.instance) {
      this.instance = new DBService();
    }
    return this.instance;
  }

  async query<T>(query: (cb: (error: Error | null, result: T | null) => void) => void): Promise<T> {
    return new Promise((resolve, reject) => {
      this.db.serialize(() => {
        query((err: Error | null, res: T | null) => {
          if (err) {
            console.log("query error: ", err);
            reject(err);
          } else {
            resolve(res as T);
          }
        });
      });
    });
  }
}

test fails with not finding the mocked prepare:

jest.mock("sqlite3");

jest.mock("./services/database/DBService.ts", () => {
  const mockInstance = {
    query: jest.fn(),
    db: {
      prepare: jest.fn()
    }
  };
  return {
    DBService: {
      getInstance: jest.fn(() => mockInstance),
    }
  };
});

beforeEach(() => {
  mockQuery = DBService.getInstance().query as jest.Mock;
  const dbMock = {
    prepare: jest.fn(() => ({
      run: jest.fn(),
    })),
  };

  // Mock sqlite3.Database constructor
  (sqlite3.Database as jest.Mock).mockImplementation(() => dbMock);
  service = SyncService.getInstance();
});

await service.attach(1);

expect(mockQuery).toHaveBeenCalledTimes(1);
expect(dbMock.prepare).toHaveBeenCalledWith(
  "UPDATE table SET boolean = true WHERE id = ?",
  id,
  expect.any(Function)
);
expect(runMock).toHaveBeenCalledTimes(1);

Solution

  • Your code have some issues:

    1. Jest support TypeScript via babel, ts-jest, see Using TypeScript, but I don't see the setup in your code.

    2. You don't need to mock sqlite3 module. You are testing the servicea.ts file, and it only depends on dbservice.ts module. So mock dbservice.ts module is enough. So you can delete __mocks__ folder.

    3. Mix callbacks and async/await in JS is not recommended. See Can I mix callbacks and async/await patterns in NodeJS?

    4. You should mock the DBService's .query() method and call the query callback and its callback, so that await service.attach(1) statement will wait for all asynchronous code to finish executing.

    5. The moduleName argument of jest.mock() should be ../dbservice, not ../../dbservice

    __tests__/servicea.ts:

    import { ServiceA } from '../servicea';
    import { DBService } from '../dbservice';
    
    jest.mock('../dbservice', () => {
      const queryCallback = jest.fn();
      const mockInstance = {
        initDatabase: jest.fn(),
        query: jest.fn().mockImplementation((query) => query(queryCallback)),
        db: {
          prepare: jest.fn().mockReturnThis(),
          run: jest.fn(),
        },
      };
      return {
        DBService: {
          getInstance: jest.fn(() => mockInstance),
        },
      };
    });
    
    describe('DB Service Tests', () => {
      const id = 1;
      let service: ServiceA;
      let mockQuery: jest.Mock;
      let mockPrepare: jest.Mock;
    
      beforeEach(() => {
        mockQuery = DBService.getInstance().query as jest.Mock;
        mockPrepare = DBService.getInstance().db.prepare as jest.Mock;
        service = ServiceA.getInstance();
      });
    
      afterEach(() => {
        jest.clearAllMocks();
      });
    
      it('should call database instance correctly', async () => {
        await service.attach(1);
    
        expect(mockQuery).toHaveBeenCalledTimes(1);
        expect(mockPrepare).toHaveBeenCalledWith(
          'UPDATE tables SET boolean = true WHERE id = ?',
          id,
          expect.any(Function)
        );
      });
    });
    

    Test result:

     PASS  __tests__/servicea.test.ts
      DB Service Tests
        ✓ should call database instance correctly (5 ms)
    
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        4.755 s, estimated 5 s
    Ran all test suites.
    

    Live demo: stackblitz