用 Jest 模拟 mysql 连接

Pmi*_*its 3 mysql unit-testing node.js jestjs

尝试测试如下所示的代码:

const mysql = require('mysql2/promise');

async myFunction () {

    const db = await mysql.createConnection(options);

    const results = await db.execute('SELECT `something` from `table`;');

    await db.end();
    
    // more code ...

}
Run Code Online (Sandbox Code Playgroud)

我需要以一种允许我使用它返回的任何内容来模拟对execute函数的调用的方式来模拟 mysql 连接。

我试过mysql2/promise模拟整个模块,但当然没有用,因为createConnection被模拟的没有返回任何可以调用该execute 函数的东西。我还尝试只模拟我需要的这 3 个函数,而不是模拟整个模块,例如:

jest.mock('mysql2/promise', () => ({
    createConnection: jest.fn(() => ({
        execute: jest.fn(),
        end: jest.fn(),
    })),
}));
Run Code Online (Sandbox Code Playgroud)

但这也不起作用。任何建议都非常感谢。

fjc*_*fjc 6

我会以不同的方式处理这个问题。当你觉得你需要模拟整个第三方库进行测试时,你的应用程序中出现了一些问题。

作为一般的最佳实践,您应该始终包装第三方库。查看此讨论以了解初学者。

基本上的想法是定义您自己的接口到所需的功能,然后使用第三方库实现这些接口。在您的其余代码中,您将只针对接口工作,而不是针对第三方实现。

这有几个优点

  1. 您可以自己定义接口。它通常比整个第三方库小得多,因为您很少使用该第三方库的所有功能,并且您可以决定什么是具体用例的最佳接口定义,而不必完全遵循某些图书馆作者决定你。
  2. 如果有一天您决定不想再使用 MySQL 而是转而使用 Mongo,那么您只需编写 DB 接口的 Mongo 实现即可。
  3. 就您而言,最重要的是:您可以轻松创建数据库接口的模拟实现,而无需开始模拟整个第三方 API。

那么这怎么可能呢?

首先,定义一个接口,因为它在您的代码中最有用。也许,您的数据库接口可能如下所示:

interface Database<T> {
  create(object: T): Promise<void>;
  get(id: string): Promise<T>;
  getAll(): Promise<T[]>;
  update(id: string, object: T): Promise<void>;
  delete(id: string): Promise<void>;
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以针对这个Database界面开发整个代码库。当您需要从“表”中检索数据时,您可以使用您的Database实现,而不是在整个代码中编写 MySQL 查询。

我将在ResultRetriever这里举一个非常原始的例子,但可以达到目的:

class ResultRetriever {
  
    constructor(private database: Database<Something>) {}

    getResults(): Promise<Something[]> {
        return this.database.getAll();
    }
  
}
Run Code Online (Sandbox Code Playgroud)

如您所见,您的代码不需要关心哪个 DB 实现提供了数据。此外,我们倒立的依赖性在这里:ResultReteriver注射Database实例。它不知道Database它得到了哪个具体实现。它不需要。它所关心的只是它是一个有效的。

您现在可以轻松实现 MySQLDatabase类:

class MySqlDatabase<T> implements Database<T> {

  create(object: T): Promise<void> {...}

  get(id: string): Promise<T> {...}

  getAll(): Promise<T[]> {
      const db = await mysql.createConnection(options);
      const results = await db.execute('SELECT `something` from `table`;');
      await db.end();
      return results;
  }

  update(id: string, object: T): Promise<void> {...}

  delete(id: string): Promise<void> {...}

}
Run Code Online (Sandbox Code Playgroud)

现在我们已经从您的主要代码库中完全抽象出特定于 MySQL 的实现。在测试方面,您可以编写一个简单的MockDatabase

export class MockDatabase<T> implements Database<T> {

  private objects: T[] = [];

  async create(object: T): Promise<void> {
    this.objects.push(object);
  }

  async get(id: string): Promise<T> {
    return this.objects.find(o => o.id === id);
  }

  async getAll(): Promise<T[]> {
    return this.objects;
  }

  update(id: string, object: T): Promise<void> {...}

  delete(id: string): Promise<void> {...}
  
}
Run Code Online (Sandbox Code Playgroud)

在测试方面,您现在可以ResultRetrieve使用您的MockDatabase而不是依赖 MySQL 库进行测试,因此完全模拟它:

describe('ResultRetriever', () => {

    let retriever: ResultRetriever;
    let db: Database;

    beforeEach(() => {
      db = new MockDatabase();
      retriever = new ResultRetriever(db);
    });

    ...

});
Run Code Online (Sandbox Code Playgroud)

如果我有点超出了问题的范围,我很抱歉,但我觉得仅仅回答如何模拟 MySQL 库并不能解决底层的架构问题。

如果您不使用/不想使用 TypeScript,则可以将相同的逻辑应用于 JavaScript。