【问题标题】:Mock mysql connection with Jest用 Jest 模拟 mysql 连接
【发布时间】:2020-08-26 11:05:42
【问题描述】:

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

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 ...

}

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

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

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

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

【问题讨论】:

    标签: mysql node.js unit-testing jestjs


    【解决方案1】:

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

    作为一般的最佳做法,您应该始终包装第三方库。初学者请查看this discussion

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

    这有几个优点

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

    那么这怎么可能呢?

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

    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>;
    }
    

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

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

    class ResultRetriever {
      
        constructor(private database: Database<Something>) {}
    
        getResults(): Promise<Something[]> {
            return this.database.getAll();
        }
      
    }
    

    如您所见,您的代码不需要关心哪个数据库实现交付数据。另外,我们在这里inverted dependenciesResultReteriver注入它的Database 实例。它不知道它得到了哪个具体的Database 实现。它不需要。它只关心它是否有效。

    您现在可以轻松实现 MySQL Database 类:

    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> {...}
    
    }
    

    现在我们已经从您的主要代码库中完全抽象出 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> {...}
      
    }
    

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

    describe('ResultRetriever', () => {
    
        let retriever: ResultRetriever;
        let db: Database;
    
        beforeEach(() => {
          db = new MockDatabase();
          retriever = new ResultRetriever(db);
        });
    
        ...
    
    });
    

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

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


    【讨论】:

    • 感谢您的回答,它让我很好地理解了我应该如何构建事物,我非常感谢,我将不得不对这个看起来很有趣的主题做更多的阅读。我仍然想知道的一件事是,即使采用这种方法,在实际测试 MySqlDatabase 实现时,我不会再次面临同样的情况吗?
    • 我会提出一个问题,使用模拟数据测试 MySqlDatabase 实现是否有任何用途。最后,如果您有一个仅在数据库接口和 MySql 库之间转换的瘦实现,那么您通过模拟测试的只是您的模拟是否正确工作,但它不会说明您的 MySQL 实现是否真的有效。因此,如果您想测试您的 MySQL 实现,我会争辩说,请针对(临时)实际 MySQL DB 执行此操作。您肯定可以在测试之前/之后上下旋转一个。
    • 我希望我的构建中断,例如,如果该外部库上的更新可能会破坏我的代码,或者如果开发人员删除了结束与数据库连接的调用。那么,在测试与数据库对话的代码时,您是在建议编写集成测试而不是单元测试?
    • 您真正关心的是您的 MySQL 实现是否正常工作,对吗?所以我会为你的 MySQL 实现编写一个测试套件,它在后台有一个实际运行的 MySQL 数据库。然后,您可以确保实现实际上是端到端的。我猜这在某种程度上是集成和单元测试的混合体
    • 这是一个很好的建议。即使是一个非常简单的接口,它只实现了一个“query()”函数,你传递一个查询字符串并返回一个承诺,也可以方便地进行测试。测试可以模拟解析的值或reject.throw 结果。
    【解决方案2】:

    如果您真正想做的只是模拟您的 MySQL 调用,则有一种“蛮力”方式。以下代码使用 TypeScript,但应该很容易适应常规 JavaScript。

    import * as mysql from "mysql2/promise";
    import { mocked } from "ts-jest/utils";
    jest.mock("mysql2/promise");
    
    async function dbMethod(conn: mysql.Pool, field1Value: number): Promise<any> {
        return await (conn.execute("SELECT field_1, field_2 FROM foo WHERE field_1=?",
           [field1Value]) as Promise<mysql.RowDataPacket[]>);
    }
    
    describe("dbMethod", () => {
       let mockDB: mysql.Pool;
       beforeEach(() => {
          mockDB =  {
             execute: jest.fn()
          } as unknown as mysql.Pool;
       });
       it("should get something from database", async () => {
          const mockExecute = mocked(mockDB.execute);
          const testData = [{
             field_1: 123,
             field_2: 456
          }];
    
          mockExecute.mockResolvedValue([
             [testData] as mysql.RowDataPacket[],
             []
          ]);
    
          // Confirm results back from MySQL
          await expect(dbMethod(mockDB, 123)).resolves.toEqual([[testData], []]);
    
          // If you want, you can confirm MySQL execute was called as expected
          expect(mockExecute).toHaveBeenCalledWith(
             "SELECT field_1, field_2 FROM foo WHERE field_1=?",
             [123]
          );            
       });
    });
    

    【讨论】:

    • 我觉得这很优雅,而且相当易读;)
    猜你喜欢
    • 2021-08-13
    • 1970-01-01
    • 2020-09-03
    • 2015-03-03
    • 2021-09-14
    • 2020-04-21
    • 2019-09-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多