如何确保 Jest bootstrap 文件优先于 运行?

How to ensure the Jest bootstrap file is run first?

简介

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

的新手

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

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

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

代码

这是安装文件,简洁明了:

// Little fix for Jest, see 
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 只是 usernames/passwords,不值得在这里显示。

如您所见,此代码使用实用程序数据库 class,即:

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. "Here is the bootstrap"
  2. "Here is the test class"
  3. 测试到此结束
  4. "Failed to clear down database"
  5. "Finished once-off db setup"
  6. 笑话报道"Jest did not exit one second after the test run has completed. This usually means that there are asynchronous operations that weren't stopped in your tests."
  7. Jest 挂起,需要 ^C 退出

我要:

  1. "Here is the bootstrap"
  2. "Finished once-off db setup"
  3. "Here is the test class"
  4. 调用时没有错误truncateTables

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

备注

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

我还尝试使用 await 针对所有 promise-returning 数据库操作编写安装文件。但是,这意味着我必须将 initDatabase 声明为 async,我认为这意味着我不能再 gua运行tee 整个安装文件首先是 运行,这本质上和我现在遇到的问题一样。

我注意到 TestDatabase return 中的大多数实用方法都是一个 promise,我对此非常满意。然而 connect 是一个奇怪的东西——我希望它来存储连接,所以我很困惑我是否可以 return 一个 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,但如果可能的话,我宁愿坚持(并且完全理解)承诺。

您需要 await 您的 database.connect()beforeEach()

我有办法解决这个问题。我还没有 au fait Jest 的微妙之处,我想知道我是否刚刚找到一个。

我的感觉是,由于从 bootstrap 到 Jest 没有 return 值,因此无法通知它需要等待承诺解决后再转到测试。其结果是承诺在测试等待期间得到解决,这会产生绝对的混乱。

也就是说bootstrap脚本只能用于同步调用

解决方案 1

一个解决方案是将 thenable 链从 bootstrap 文件移动到新的 beforeAll() 挂钩。我将 connect 方法转换为 return Promise,因此它的行为与其他方法一样,值得注意的是我在新钩子和现有钩子中 returned Promise 链的值一。我相信这会通知 Jest promise 需要在其他事情发生之前解决。

这是新的测试文件:

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 class 中 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,它也必须在退出前完成所有异步工作。