If you are using "vi.mock" factory, make sure there are no top level variables inside

41 Views Asked by At

I'm new to testing and would like to learn how to mock the mysql2 database as an instance of Kysely with vitest. I've been following a thread provided from GitHub, but I encountered the error as the title it says:

database.ts

import * as dotenv              from "dotenv";
import { type DB }              from "../types/schema.types";
import { createPool, }          from "mysql2";
import { Kysely, MysqlDialect } from "kysely";
dotenv.config();

export const dialect = new MysqlDialect({
  pool: createPool({
    database: `${process.env.DATABASE}`,
    host: `${process.env.DATABASE_HOST}`,
    port: process.env.DATABASE_PORT as unknown as number,
    user: `${process.env.USER}`,
    password: process.env.PASSWORD,
    waitForConnections: true,
    multipleStatements: true,
    charset: "utf8mb4",
    connectionLimit: `${process.env.DATABASE_CONNECTION_LIMIT}` as unknown as number,
    queueLimit: 0,
  }),
})

const db = new Kysely<DB>({ dialect });
export default db;

sample.spec.ts

import { it, vi, expect } from "vitest";
import db from "../database/database";
import { DB } from "../types/schema.types";
import { Kysely, MysqlDialect } from "kysely";

// Test case
it("Get user count", async () => {
  const mocks = vi.hoisted(() => ({ query: vi.fn() }));
  
  // Mocking database module
  vi.mock("../database/database", async (importOriginal) => {
    const actualModule = await importOriginal<typeof import("../database/database")>();
    return {
      ...actualModule,
      __esModule: true,
      ownerDb: new Kysely<DB>({
        dialect: new MysqlDialect({ pool: vi.fn() }),
      }),
    };
  });

  mocks.query.mockResolvedValue({ rows: [{ count: 1 }] });

  const query = db
    .selectFrom("users")
    .select((eb) => eb.fn.count<number>("users.user_id").as("count"));

  const { count } = await db
    .selectNoFrom((eb) => eb.fn.coalesce(query, eb.lit(0)).as("count"))
    .executeTakeFirstOrThrow();

  expect(count).toBe(1);

full error: enter image description here

1

There are 1 best solutions below

2
On

From the vi.mock documentation...

vi.mock is hoisted (in other words, moved) to top of the file. It means that whenever you write it (be it inside beforeEach or test), it will actually be called before that.

That means what's really run is this:

import { it, vi, expect } from "vitest";

// Mocking database module
vi.mock("../database/database", async (importOriginal) => {
  const actualModule = await importOriginal<typeof import("../database/database")>();
  return {
    ...actualModule,
    __esModule: true,
    ownerDb: new Kysely<DB>({
      dialect: new MysqlDialect({ pool: vi.fn() }),
    }),
  };
});

import db from "../database/database";
import { DB } from "../types/schema.types";
import { Kysely, MysqlDialect } from "kysely";

// Test case
it("Get user count", async () => {
  const mocks = vi.hoisted(() => ({ query: vi.fn() }));

The block passed to vi.mock cannot see DB. You can use vi.hoisted on the import. Then the import will be hoisted along with the call to vi.mock.

import { it, vi, expect } from "vitest";
import db from "../database/database";
import { Kysely, MysqlDialect } from "kysely";
const { DB } = vi.hoisted(() => await import('../types/schema.types'));

See A Practical Guide to Mocking Svelte Stores with Vitest.


However, hoisting makes the code very difficult to understand and vi.mock is loaded with caveats. Consider using vi.doMock instead. Note that you will have to dynamically import ../database/database.