如何使用 jest 在 node.js 中模拟 postgresql (pg)

How to Mock postgresql (pg) in node.js using jest

我是 node.js 的新人。我正在 node.js 中为 postgresql 编写代码,使用 pgpg-native 为无服务器应用程序编写代码。我需要为它编写单元测试。我无法使用 jest 或 sinon 模拟 pg 客户端。

我的实际代码是这样的

const { Client } = require('pg');
export const getAlerts = async (event, context) => {

  const client = new Client({
    user: process.env.DB_USER,
    host: process.env.DB_HOST,
    database: process.env.DB_DATABASE,
    password: process.env.DB_PASSWORD,
    port: process.env.PORT
  });

  await client.connect();

  try {
    const result = await client.query(`SELECT * FROM public.alerts;`);
    console.log(result.rows);
    client.end();
    return success({ message: `${result.rowCount} item(s) returned`, data: result.rows, status: true });

  } catch (e) {
    console.error(e.stack);
    client.end();
    return failure({ message: e, status: false });
  }

};

这里如何模拟pg客户端?

这里是使用jestjs的单元测试解决方案:

index.js:

const { Client } = require('pg');
const { success, failure } = require('./handler');

export const getAlerts = async (event, context) => {
  const client = new Client({
    user: process.env.DB_USER,
    host: process.env.DB_HOST,
    database: process.env.DB_DATABASE,
    password: process.env.DB_PASSWORD,
    port: process.env.PORT,
  });

  await client.connect();

  try {
    const result = await client.query(`SELECT * FROM public.alerts;`);
    console.log(result.rows);
    client.end();
    return success({ message: `${result.rowCount} item(s) returned`, data: result.rows, status: true });
  } catch (e) {
    console.error(e.stack);
    client.end();
    return failure({ message: e, status: false });
  }
};

hander.js:

export function success(data) {}
export function failure(data) {}

index.spec.js:

import { getAlerts } from './';
import { Client } from 'pg';
import { success, failure } from './handler';

jest.mock('pg', () => {
  const mClient = {
    connect: jest.fn(),
    query: jest.fn(),
    end: jest.fn(),
  };
  return { Client: jest.fn(() => mClient) };
});

jest.mock('./handler.js', () => {
  return {
    success: jest.fn(),
    failure: jest.fn(),
  };
});

describe('59540432', () => {
  let client;
  beforeEach(() => {
    client = new Client();
  });
  afterEach(() => {
    jest.clearAllMocks();
  });
  it('should success', async () => {
    client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
    await getAlerts();
    expect(client.connect).toBeCalledTimes(1);
    expect(client.query).toBeCalledWith('SELECT * FROM public.alerts;');
    expect(client.end).toBeCalledTimes(1);
    expect(success).toBeCalledWith({ message: '0 item(s) returned', data: [], status: true });
  });

  it('should failure', async () => {
    const mError = new Error('dead lock');
    client.query.mockRejectedValueOnce(mError);
    await getAlerts();
    expect(client.connect).toBeCalledTimes(1);
    expect(client.query).toBeCalledWith('SELECT * FROM public.alerts;');
    expect(client.end).toBeCalledTimes(1);
    expect(failure).toBeCalledWith({ message: mError, status: false });
  });
});

100% 覆盖率的单元测试结果:

 PASS  src/Whosebug/59540432/index.spec.js (11.792s)
  59540432
    ✓ should success (16ms)
    ✓ should failure (5ms)

  console.log src/Whosebug/59540432/index.js:3131
    []

  console.error src/Whosebug/59540432/index.js:3155
    Error: dead lock
        at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/Whosebug/59540432/index.spec.js:39:20
        at step (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/Whosebug/59540432/index.spec.js:33:23)
        at Object.next (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/Whosebug/59540432/index.spec.js:14:53)
        at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/Whosebug/59540432/index.spec.js:8:71
        at new Promise (<anonymous>)
        at Object.<anonymous>.__awaiter (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/Whosebug/59540432/index.spec.js:4:12)
        at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/Whosebug/59540432/index.spec.js:38:24)
        at Object.asyncJestTest (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
        at resolve (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
        at new Promise (<anonymous>)
        at mapper (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
        at promise.then (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
        at process._tickCallback (internal/process/next_tick.js:68:7)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 index.js |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        14.109s

源代码:https://github.com/mrdulin/jest-codelab/tree/master/src/Whosebug/59540432

我不建议任何人使用它,可能会有一些错误,但它让我 100% 的单元测试覆盖率 for-now...

它涵盖了带有回调的池 API 而不是每个人都关注的客户端 API。

jest.mock('pg')
const { Pool } = require('pg')
const mockPgPoolOn = jest.fn((_event, callback) => { callback() })
const mockPgPoolConnectClientClose = jest.fn(() => {})
const mockPgPoolConnectQueryDoneFn = jest.fn(() => {
  mockPgPoolConnectClientClose()
})
let mockPgPoolConnectErr = null
let mockPgPoolConnectQueryErr = null
let mockPgPoolConnectQueryResult = null
const mockPgPoolConnectQuery = jest.fn((_text, _params, callback) => {
  callback(
    mockPgPoolConnectQueryErr,
    mockPgPoolConnectQueryResult
  )
})
const mockPgPoolConnectClient = jest.fn(() => {
  return {
    query: mockPgPoolConnectQuery,
    on: mockPgPoolOn,
    close: mockPgPoolConnectClientClose
  }
})
const mockPgPoolConnect = jest.fn((callback) => {
  callback(
    mockPgPoolConnectErr,
    mockPgPoolConnectClient(),
    mockPgPoolConnectQueryDoneFn
  )
})
let mockPgPoolQueryErr = null
let mockPgPoolQueryResult = null
const mockPgPoolQuery = jest.fn((_text, _params, callback) => {
  callback(mockPgPoolQueryErr, mockPgPoolQueryResult)
})
Pool.mockImplementation((_config) => {
  return {
    connect: mockPgPoolConnect,
    query: mockPgPoolQuery,
    on: mockPgPoolOn
  }
})

const db = require('./index') // My module that uses require('pg')

我也有 clean-up 的处理程序,因为这是我开始定义任何东西之前的全部...

afterEach(() => {
  mockPgPoolQuery.mockClear()
  mockPgPoolConnect.mockClear()
  // ...

  mockPgPoolQueryErr = null
  mockPgPoolConnectErr = null
  mockPgPoolConnectQueryErr = null
  mockPgPoolConnectQueryResult = null
  mockPgPoolQueryResult = null
  // ...
})

这似乎是很多工作,就像我说的,虽然它给我的覆盖范围 1:1 与观察到的用法相匹配,但似乎很多。

我正在使用回调 API,因为它是 pg 依赖项记录的内容...到目前为止不喜欢它。

这是一个老问题,但这里有一个新答案:

你可以看看 pg-mem,这是我最近发布的一个模拟 in-memory postgres 实例的库。

它支持大多数常见的 SQL 查询(但会在不太频繁的语法上失败 - 如果遇到这种情况,请提出问题)。

我写了一篇关于它的文章here

对于您的用例(如何与 pg 一起使用),请参阅 this wiki section

这是我的设置

const { Pool } = require('pg');

// setup for to mock pg
jest.mock('pg', () => {
  const mPool = {
    connect: function () {
      return { query: jest.fn() };
    },
    query: jest.fn(),
    end: jest.fn(),
    on: jest.fn(),
  };
  return { Pool: jest.fn(() => mPool) };
});

describe('test for handle relay action', () => {
  let pool;
  // before each test case
  beforeEach(() => {
    pool = new Pool();
  });
  // clean up after each test case done
  afterEach(() => {
    jest.clearAllMocks();
  });

  test('should test', async () => {
    \* implement your test here *\
    
    pool.query.mockResolvedValue({ rows: [] });
    expect(pool.query).toBeCalledTimes(3);
    expect(pool.query).toHaveBeenCalledWith("check some texts");
    expect(pool.query).toHaveBeenCalledWith(expect.anything());
 });