【问题标题】:How to ensure the Jest bootstrap file is run first?如何确保首先运行 Jest 引导文件?
【发布时间】:2020-05-07 00:57:05
【问题描述】:

简介

我在 Jest 测试框架中使用的数据库 Promises 卡住了。事情以错误的顺序运行,并且在我最近的一些更改之后,Jest 没有正确完成,因为没有处理未知的异步操作。我对 Node/Jest 还很陌生。

这就是我想要做的。我在多个 Docker 容器环境中设置 Jest 以调用内部 API 以测试其 JSON 输出,并运行服务函数以查看它们对测试环境 MySQL 数据库所做的更改。为此,我是:

  • 使用setupFilesAfterEnv Jest 配置选项指向一个安装文件,我认为应该首先运行该文件
  • 使用安装文件销毁测试数据库(如果存在),重新创建它,然后创建一些表
  • 使用mysql2/promise对数据库进行操作
  • 在测试中使用beforeEach(() => {}) 截断所有表,准备插入每个测试的数据,以便测试不相互依赖

我可以确认 Jest 的设置文件在第一个(也是唯一一个)测试文件之前运行,但奇怪的是测试文件中的 Promise catch() 似乎在 finally 之前抛出设置文件。

我会先写下我的代码,然后推测我隐约怀疑的问题。

代码

这是设置文件,简洁明了:

// Little fix for Jest, see https://stackoverflow.com/a/54175600
require('mysql2/node_modules/iconv-lite').encodingExists('foo');

// Let's create a database/tables here
const mysql = require('mysql2/promise');
import TestDatabase from './TestDatabase';
var config = require('../config/config.json');

console.log('Here is the bootstrap');

const initDatabase = () => {
  let database = new TestDatabase(mysql, config);
  database.connect('test').then(() => {
    return database.dropDatabase('contributor_test');
  }).then(() => {
    return database.createDatabase('contributor_test');
  }).then(() => {
    return database.useDatabase('contributor_test');
  }).then(() => {
    return database.createTables();
  }).then(() => {
    return database.close();
  }).finally(() => {
    console.log('Finished once-off db setup');
  });
};
initDatabase();

config.json 只是用户名/密码,不值得在这里显示。

如您所见,这段代码使用了一个实用数据库类,即:

export default class TestDatabase {

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

  async connect(environmentName) {
    if (!environmentName) {
      throw 'Please supply an environment name to connect'
    }
    if (!this.config[environmentName]) {
      throw 'Cannot find db environment data'
    }

    const config = this.config[environmentName];
    this.connection = await this.mysql.createConnection({
      host: config.host, user: config.username,
      password: config.password,
      database: 'contributor'
    });
  }

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

    return this.connection;
  }

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

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

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

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

  /**
   * This will be replaced with the migration system
   */
  createTables() {
    return Promise.all(
      this.getTables().map(table => this.createTable(table))
    );
  }

  /**
   * This will be replaced with the migration system
   */
  createTable(table) {
    return this.getConnection().query(
      `CREATE TABLE IF NOT EXISTS ${table} (id INTEGER)`
    );
  }

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

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

  close() {
    this.getConnection().close();
  }

}

最后,这是实际测试:

const mysql = require('mysql2/promise');
import TestDatabase from '../TestDatabase';
var config = require('../../config/config.json');

let database = new TestDatabase(mysql, config);

console.log('Here is the test class');


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

  beforeEach(() => {
    database.connect('test').then(() => {
      return database.useDatabase('contributor_test');
    }).then (() => {
      return database.truncateTables();
    }).catch(() => {
      console.log('Failed to clear down database');
    });
  });

  afterAll(async () => {
    await database.getConnection().close();
  });

  test('Describe this demo test', () => {
    expect(true).toEqual(true);
  });

});

输出

如你所见,我有一些控制台日志,这是他们意想不到的顺序:

  1. “这是引导程序”
  2. “这里是测试类”
  3. 测试到此结束
  4. “清除数据库失败”
  5. “已完成一次性数据库设置”
  6. Jest 报告“Jest 在测试运行完成后一秒钟没有退出。这通常意味着在您的测试中存在未停止的异步操作。”
  7. Jest 挂起,需要 ^C 退出

我想要:

  1. “这是引导程序”
  2. “已完成一次性数据库设置”
  3. “这里是测试类”
  4. 调用truncateTables时没有错误

我怀疑数据库错误是TRUNCATE 操作失败,因为表还不存在。当然,如果命令以正确的顺序运行,它们就会!

注意事项

我最初是导入mysql 而不是mysql/promise,并从 Stack Overflow 上的其他地方发现,如果没有承诺,需要为每个命令添加回调。这会使设置文件变得混乱 - 每个操作连接、删除数据库、创建数据库、使用数据库、创建表、关闭都需要出现在深度嵌套的回调结构中。我可能会这样做,但它有点恶心。

我还尝试使用await 针对所有返回承诺的数据库操作编写设置文件。但是,这意味着我必须将initDatabase 声明为async,我认为这意味着我不能再保证整个安装文件首先运行,这本质上与我现在遇到的问题相同。

我注意到TestDatabase 中的大多数实用程序方法都返回了一个承诺,我对此非常满意。然而connect 是一个奇怪的东西——我希望它来存储连接,所以我对是否可以返回 Promise 感到困惑,因为 Promise 不是连接。我刚刚尝试使用.then() 来存储连接,如下所示:

    return this.mysql.createConnection({
      host: config.host, user: config.username,
      password: config.password
    }).then((connection) => {
      this.connection = connection;
    });

我想知道这是否可行,因为 thenable 链应该等待连接承诺解决,然后再移动到列表中的下一个事物。但是,会产生同样的错误。

我短暂地认为使用两个连接可能会出现问题,以防在一个连接中创建的表在该连接关闭之前无法看到。基于这个想法,也许我应该尝试在设置文件中连接并以某种方式重新使用该连接(例如,通过使用 mysql2 连接池)。但是我的感觉告诉我,这确实是一个 Promise 问题,我需要在 Jest 尝试继续测试执行之前弄清楚如何在设置文件中完成我的 db init。

接下来我可以尝试什么?如果这是一种更好的方法,我愿意放弃mysql2/promise 并回退到mysql,但如果可能的话,我宁愿坚持(并完全理解)承诺。

【问题讨论】:

  • 我收到一些场外反馈,beforeEach() 需要返回其内容,以通知 Jest 它正在执行异步操作。我也会试试的。

标签: javascript async-await jestjs es6-promise


【解决方案1】:

你需要在beforeEach()await你的database.connect()

【讨论】:

  • 好的,这是一个很好的开始 - 非常感谢。 Jest 现在完成了,没有关于未处理的异步操作的投诉。但是,我已经在我的两个 thenable 链中添加了一些战略控制台日志记录,并且我发现测试中的表截断是在设置文件中创建表之前尝试的。不知何故,我需要让 Jest 等待安装文件完成,而不是让异步请求在开始测试之前完成(并且在技术上仍然处于挂起状态)。
  • 我可以尝试使用非 Promise 版本的数据库库,但据我了解 - 这不是非常公认的 - 这将需要一个深度的回调树。我什至不确定 Jest 是否会等他们回来!
  • 我可以尝试一些可怕的东西,比如在设置文件末尾的循环,它反复测试数据库的状态(或者可能是由 Promise finally() 设置的标志)。但这甚至比嵌套回调 :=p 更糟糕。
  • 我正在考虑问题是否主要出在 Jest 中,并且我应该在每次测试中在 beforeAll() 中进行所有设置,而不是尝试从引导程序运行它。也许这只是没有准备好等待异步操作完成?
【解决方案2】:

我有一个解决方案。我还没有au fait了解 Jest 的微妙之处,我想知道我是否刚刚找到一个。

我的感觉是,由于从引导程序到 Jest 没有返回值,因此无法通知它需要等待 promise 解决后再进行测试。这样做的结果是,在等待测试期间承诺正在解决,这会产生绝对的混乱。

也就是说,引导脚本只能用于同步调用。

解决方案 1

一种解决方案是将 thenable 链从引导文件移动到新的 beforeAll() 挂钩。我将connect 方法转换为返回一个Promise,因此它的行为与其他方法一样,特别是我在新钩子和现有钩子中都returned 了Promise 链的值。我相信这会通知 Jest,承诺需要在其他事情发生之前解决。

这是新的测试文件:

const mysql = require('mysql2/promise');
import TestDatabase from '../TestDatabase';
var config = require('../../config/config.json');

let database = new TestDatabase(mysql, config);

//console.log('Here is the test class');

beforeAll(() => {
  return database.connect('test').then(() => {
    return database.dropDatabase('contributor_test');
  }).then(() => {
    return database.createDatabase('contributor_test');
  }).then(() => {
    return database.useDatabase('contributor_test');
  }).then(() => {
    return database.createTables();
  }).then(() => {
    return database.close();
  }).catch((error) => {
    console.log('Init database failed: ' +  error);
  });
});

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

  beforeEach(() => {
    return database.connect('test').then(() => {
      return database.useDatabase('contributor_test');
    }).then (() => {
      return database.truncateTables();
    }).catch((error) => {
      console.log('Failed to clear down database: ' + error);
    });
  });

  /**
   * I wanted to make this non-async, but Jest doesn't seem to
   * like receiving a promise here, and it finishes with an
   * unhandled async complaint.
   */
  afterAll(() => {
    database.getConnection().close();
  });

  test('Describe this demo test', () => {
    expect(true).toEqual(true);
  });

});

事实上,这可能可以进一步简化,因为不需要关闭和重新打开连接。

这是 TestDatabase 类中 connect 的非异步版本,以配合上述更改:

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

    const config = this.config[environmentName];

    return this.mysql.createConnection({
      host: config.host, user: config.username,
      password: config.password
    }).then(connection => {
      this.connection = connection;
    });
  }

此解决方案的缺点是:

  • 我必须在每个测试文件中调用这个初始化代码(重复的工作我只想做一次),或者
  • 我必须只在第一个测试中调用这个初始化代码(这有点脆弱,我假设测试是按字母顺序运行的?)

解决方案 2

一个比较明显的解决方案是我可以把数据库初始化代码放到一个完全独立的进程中,然后修改package.json的设置:

"test": "node ./bin/initdb.js && jest tests"

我没有尝试过,但我很确定它会起作用 - 即使初始化代码是 JavaScript,它也必须在退出之前完成所有异步工作。

【讨论】:

  • 刚才有时间看看这个。很高兴我至少能帮上忙!我想除了您自己的建议之外,我唯一的其他建议是您继续更改设置文件以使所有数据库调用await,然后还有awaitinitDatabase() 的调用......但干得好!跨度>
  • 确实@Ashley,我认为在您和不在 Stack Overflow 上的朋友之间,提供了足够的橡皮鸭灵感来解决这个问题!他还建议this feature,它似乎支持异步调用,所以我想我也会尝试一下——它看起来比我目前提供的解决方案更干净。不过,我已经走出困境,所以谢谢你。
  • 我担心这就是“了解 Jest”和“已经与 Jest 在战壕中待了几年,并且有伤痕来证明这一点”之间的区别:=O
  • @Ashley:关于在设置文件中使用await,我很确定我试过了,但也许我的数据库实用程序类从那时起已经变形了,如果我要重试,它现在可以工作了。无论如何,我将在版本控制中快速捕获此代码,因为它有时会觉得 Node 代码只有在月球处于其轨道的正确相位时才有效。
  • 我仍然对 Jest 和 Node 感到满意,所以我听说了。很高兴你现在已经准备好了,祝你结节快乐!
猜你喜欢
  • 2012-03-12
  • 2012-02-06
  • 1970-01-01
  • 2014-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-13
相关资源
最近更新 更多