如何确保首先运行Jest引导文件



简介

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

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

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

我可以确认Jest的安装文件在第一个(也是唯一一个)测试文件之前运行,但奇怪的是,测试文件中的Promisecatch()似乎在安装文件中的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的其他地方发现,如果没有promise,就需要向每个命令添加回调。这会使安装文件变得混乱——连接、删除数据库、创建数据库、使用数据库、创建表、关闭的每个操作都需要出现在嵌套很深的回调结构中。我也许可以做,但有点恶心。

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

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

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

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

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

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

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

我有一个解决方案。我还不了解耶稣的微妙之处,我想知道我是否刚刚找到了一个。

我的感觉是,由于引导程序没有返回值给Jest,因此无法通知它需要等待承诺得到解决,然后再进行测试。这样做的结果是,在等待测试期间,承诺得到了解决,这造成了一片混乱。

换句话说,引导脚本只能用于同步调用。

解决方案1

一种解决方案是将可用链从引导文件移动到新的beforeAll()钩子。我转换了connect方法以返回Promise,因此它的行为与其他方法类似,值得注意的是,我在新钩子和现有钩子中都使用了return来计算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;
});
}

这种解决方案的缺点是:

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

解决方案2

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

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

我还没有尝试过,但我非常确信这会奏效——即使init代码是JavaScript,它也必须在退出之前完成所有异步工作。

最新更新