3

我在 Jest 中遇到了异步代码的进一步问题。我之前的问题(针对同一个项目)与在 Jest 引导程序中运行异步代码有关。我的新问题与在测试中运行异步数据库调用有关。我的目标是连接到数据库服务并进行调用以确保它们正确读取和写入数据库。我在一个 Docker 容器中运行测试,连接到另一个容器中的 MySQL 实例。

我正在使用mysql2/promiseNode 库,正如同样的建议,它将基于回调的数据库操作包装在 Promise 中。大多数操作都是异步的,除了连接关闭(和其他一些)。确实,我想知道这是否相关。

我应该从一些代码开始。这是我的测试:

import TestDatabase from '../TestDatabase';
var config = require('../../config/config.json');
import FetchDonations from "../../src/services/FetchDonations";
const envName = 'test';

let database = new TestDatabase(config);

// Connect before all tests
beforeAll(() => {
  console.log('Connect Jest database');
  return database.connect(envName);
});

// Disconnect after all tests
afterAll(async done => {
  console.log('Disconnect Jest database');
  database.close();
  done();
});

describe('Database tests', () => {

  // Before every test
  beforeEach(() => database.beforeEachTest(envName));

  test('Describe this demo test', () => {
    console.log('Test #1');
    expect(true).toEqual(true);
  });

  test('Describe this demo test 2', () => {
    console.log('Test #2');
    expect(true).toEqual(true);
  });

});

这只是运行几个虚拟测试。他们什么都不做,我只是想让之前/之后的钩子工作。这些是他们应该做的:

  • beforeAll - 连接数据库一次(异步操作)
  • afterAll - 与数据库断开一次(mysql2中的同步操作)
  • beforeEach -database.beforeEachTest()在每次测试之前运行,这会截断数据库中的表(异步操作)

如下TestDatabase所示 - 这些是我编写的用于帮助进行数据库测试的实用方法:

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

export default class TestDatabase {

  constructor(config) {
    this.config = config;
  }

  beforeEachTest(environmentName) {
    console.log('Before a test');

    return this.setForeignKeyChecks(false).then(() => {
      return this.truncateTables();
    }).then(() => {
      return this.setForeignKeyChecks(true);
    }).catch((error) => {
      console.log('Failed to clear down database: ' + error);
    });
  }

  connect(environmentName) {
    const config = this.getEnvConfig(environmentName);

    return mysql.createConnection({
      host: config.host, user: config.username,
      password: config.password
    }).then((connection) => {
      this.connection = connection;
      return this.useDatabase(environmentName);
    }).catch((error) => {
      console.log('Failed to connect to the db');
    });
  }

  getConnection() {
    if (!this.connection) {
      throw 'Database not connected';
    }

    return this.connection;
  }

  dropDatabase(environmentName) {
    const config = this.getEnvConfig(environmentName);

    return this.getConnection().query(
      `DROP DATABASE IF EXISTS ${config.database}`
    );
  }


  createDatabase(environmentName) {
    const config = this.getEnvConfig(environmentName);

    return this.getConnection().query(
      `CREATE DATABASE IF NOT EXISTS ${config.database}`
    );
  }

  useDatabase(environmentName) {
    const config = this.getEnvConfig(environmentName);

    return this.getConnection().query(
      `USE ${config.database}`
    );
  }

  setForeignKeyChecks(value) {
    // Make injected value safe
    var boolStr = value ? '1' : '0';

    return this.getConnection().query(
      `SET FOREIGN_KEY_CHECKS = ${boolStr}`
    );
  }

  getTables() {
    return ['contribution', 'donation', 'expenditure',
      'tag', 'expenditure_tag'];
  }

  truncateTables() {
    return Promise.all(
      this.getTables().map(table => this.truncateTable(table))
    );
  }

  truncateTable(table) {
    return this.getConnection().query(
      `TRUNCATE TABLE ${table}`
    );
  }

  /**
   * Close is synchronous so there is no returned promise
   */
  close() {
    this.getConnection().close();
  }

  getEnvConfig(environmentName) {
    if (!environmentName) {
      throw 'Please supply an environment name'
    }
    if (!this.config[environmentName]) {
      throw 'Cannot find database environment data'
    }

    return this.config[environmentName];
  }
}

现在,如果我运行测试,它们会通过并完成,但有两个奇怪之处。首先,一些异步 console.log 输出是在测试摘要之后输出的,所以我认为我没有按照 Jest 想要的方式处理异步。换句话说,我认为应该在所有这些之后呈现摘要:

/project/node_modules/.bin/jest tests
  console.log
    Connect Jest database

      at Object.<anonymous> (tests/database/TestDemo.test.js:29:11)

  console.log
    Before a test

      at TestDatabase.beforeEachTest (tests/TestDatabase.js:10:13)

 PASS  tests/database/TestDemo.test.js
  Database tests
    ✓ Describe this demo test (72ms)
    ✓ Describe this demo test 2 (58ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.118s, estimated 3s
Ran all test suites matching /tests/i.
  console.log
    Test #1

      at Object.<anonymous> (tests/database/TestDemo.test.js:46:13)

  console.log
    Before a test

      at TestDatabase.beforeEachTest (tests/TestDatabase.js:10:13)

  console.log
    Test #2

      at Object.<anonymous> (tests/database/TestDemo.test.js:51:13)

  console.log
    Disconnect Jest database

      at _callee$ (tests/database/TestDemo.test.js:35:11)

如您所见,两个测试的输出出现在摘要之后,但beforeEach第一个测试的输出出现在测试摘要之前。

此外,如果我添加使用数据库的真实测试,我会收到错误消息说我有未处理的承诺,我应该尝试 Jest 的未处理承诺检测器 ( --detectOpenHandles)。此外,在这种情况下,Jest 会在循环中停止并需要 ^C 来返回控制台提示。

所以,我正在尝试--detectOpenHandles使用当前代码,虽然我没有得到 Jest 冻结,但我得到了以下内容。

Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  TCPWRAP

      22 |     const config = this.getEnvConfig(environmentName);
      23 | 
    > 24 |     return mysql.createConnection({
         |                  ^
      25 |       host: config.host, user: config.username,
      26 |       password: config.password
      27 |     }).then((connection) => {

      at new Connection (node_modules/mysql2/lib/connection.js:35:27)
      at Object.<anonymous>.exports.createConnection (node_modules/mysql2/index.js:10:10)
      at Object.createConnection (node_modules/mysql2/promise.js:230:31)
      at TestDatabase.connect (tests/TestDatabase.js:24:18)
      at Object.<anonymous> (tests/database/TestDemo.test.js:30:19)

我的观点是,这与我通过更多测试获得的冻结直接相关,我应该在尝试添加更多测试之前解决这个问题。

我已经通过几个调查循环来确定可能导致这种情况的原因,并且代码已经过多次调整:

  • afterAll并且beforeEach是异步操作,因此需要将它们return编入 Jest,因此 Jest 知道等待它们解决。
  • afterAll关闭数据库,但这不是异步的,所以我在done()这里使用 Jest,尽管如果没有done().
  • TestDatabase包含两个主要方法,beforeEachTestconnect,我一直非常小心地确保它们返回 Promises。
  • 我倾向于使用链式 thenables,而不是 async-await,因为它让我感觉更清楚。但是,我在几个方面尝试了 async-await,但没有帮助。
  • dropDatabase, createDatabase, setForeignKeyChecks,truncateTables之类的实用程序代码truncateTable都返回 Promises。
  • 我已经阅读了Jest 异步文档,并且有很多方法。主要的收获是,如果你正在测试异步的东西,promise 应该返回给 Jest,所以适当的等待就完成了。我真正的测试是同步的,只是我的 before 钩子是异步的。想了想,不知道是不是这个问题?

我对 Jest 还很陌生,对 JS 异步也没有太多经验。每次我认为我对异步的理解有所提高时,我都会得到一个新的曲线球。但是,我想知道这是否是更多的笑话,而不是理解原始异步的困难。

4

2 回答 2

2

最好将服务器连接代码移动到一个单独的文件中的函数中,然后将其导出,并在您的笑话测试中调用它。这可能会阻止错误open handle potentially keeping jest from exiting

使用它很危险,--forceExit因为如果在测试完成后运行,它可能会过早终止尚未完成的操作(例如数据库清理操作)。

于 2021-03-15T22:49:44.777 回答
2

现在,我已经删除--detectOpenHandles并添加了--forceExit. 顾名思义,它确保 Jest 在测试后退出,即使它认为有未处理的 Promise。

有趣的是,存在该选项 - 我想知道这意味着误报很常见。无论哪种方式,我的测试确实通过和失败,所以我将把这个问题放在次要位置。仍然非常欢迎不是解决方法的更令人满意的答案。

于 2020-05-10T11:25:30.170 回答