如何确保Jest bootstrap文件首先运行? [英] How to ensure the Jest bootstrap file is run first?

查看:73
本文介绍了如何确保Jest bootstrap文件首先运行?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在Jest测试框架中使用数据库Promises.事情以错误的顺序运行,并且按照我的一些最新更改,Jest无法正确完成,因为未处理未知的异步操作.我是Node/Jest的新手.

I am getting stuck with database Promises I am using inside the Jest testing framework. Things are running in the wrong order, and following some of my latest changes, Jest is not finishing correctly because an unknown asynchronous operation is not being handled. I am fairly new to Node/Jest.

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

Here is what I am trying to do. I am setting up Jest inside a multiple Docker container environment to call internal APIs in order to test their JSON outputs, and to run service functions in order to see what change they make against a test-environment MySQL database. To do that, I am:

  • 使用setupFilesAfterEnv Jest配置选项指向一个安装文件,我相信应该首先运行该文件
  • 使用安装文件破坏测试数据库(如果存在),重新创建它,然后创建一些表
  • 使用mysql2/promise对数据库进行操作
  • 在测试中使用beforeEach(() => {})截断所有表,以准备插入每个测试的数据,以使测试彼此之间不依赖
  • using the setupFilesAfterEnv Jest configuration option to point to a setup file, which I believe should be run first
  • using the setup file to destroy the test database (if it exists), to recreate it, and then to create some tables
  • using mysql2/promise to carry out operations on the database
  • using a beforeEach(() => {}) in a test to truncate all tables, in readiness for inserting per-test data, so that tests are not dependent on each other

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

I can confirm that the setup file for Jest is being run before the first (and only) test file, but what is odd is that a Promise catch() in the test file appears to be thrown before a finally in the setup file.

我将首先放下代码,然后猜测我隐约怀疑是什么问题.

I will put my code down first, and then speculate on what I vaguely suspect to be a problem.

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

Here is the setup file, nice and straightforward:

// 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只是用户名/密码,不值得在此处显示.

The config.json is just usernames/passwords and not worth showing here.

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

As you can see this code uses a utility database class, which is this:

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

});

输出

如您所见,我有一些控制台日志,这是他们的意外顺序:

Output

As you can see I have some console logs, and this is their unexpected order:

  1. 这是引导程序"
  2. 这是考试班"
  3. 测试在这里完成
  4. 无法清除数据库"
  5. 完成一次性数据库设置"
  6. Jest报告:"Jest在测试运行完成后一秒钟没有退出.这通常意味着您的测试中没有停止异步操作."
  7. 玩笑挂起,需要^ C退出

我想要:

  1. 这是引导程序"
  2. 完成一次性数据库设置"
  3. 这是考试班"
  4. 调用truncateTables
  5. 时没有错误
  1. "Here is the bootstrap"
  2. "Finished once-off db setup"
  3. "Here is the test class"
  4. No error when calling truncateTables

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

I suspect that the database error is that the TRUNCATE operations are failing because the tables do not exist yet. Of course, if the commands ran in the right order, they would!

我最初是导入mysql而不是mysql/promise的,然后在Stack Overflow上的其他地方发现,没有承诺,就需要向每个命令添加回调.这将使设置文件混乱-每个操作都需要连接,删除db,创建db,使用db,创建表,关闭,这些操作都需要出现在深度嵌套的回调结构中.我可能可以做到,但是有点恶心.

I originally was importing mysql instead of mysql/promise, and found from elsewhere on Stack Overflow that without promises, one needs to add callbacks to each command. That would make the setup file messy - each of the operations connect, drop db, create db, use db, create tables, close would need to appear in a deeply nested callback structure. I could probably do it, but it is a bit icky.

我还尝试使用await针对所有应许返回的db操作写入设置文件.但是,这意味着我必须将initDatabase声明为async,我认为这意味着我不能再保证整个安装文件都首先运行,从本质上讲,这是我现在遇到的问题.

I also tried writing the setup file using await against all the promise-returning db operations. However, that meant I had to declare initDatabase as async, which I think means I can no longer guarantee the whole of the setup file is run first, which is in essence the same problem as I have now.

我注意到TestDatabase中的大多数实用程序方法都返回一个诺言,对此我感到非常满意.但是connect很奇怪-我希望它存储连接,所以对于Promise不是连接,我是否可以返回Promise感到困惑.我刚刚尝试使用.then()来存储连接,就像这样:

I have noticed that most of the utility methods in TestDatabase return a promise, and I am pretty happy with those. However connect is an oddity - I want this to store the connection, so was confused about whether I could return a Promise, given that a Promise is not a connection. I have just tried using .then() to store the connection, like so:

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

我想知道这是否行得通,因为可伸缩链应该等待连接承诺解析后再移至列表中的下一个对象.但是,会产生相同的错误.

I wondered if that should work, since the thenable chain should wait for the connection promise to resolve before moving onto the next thing in the list. However, the same error is produced.

我简要地认为使用两个连接可能是一个问题,以防在关闭该连接之前看不到在一个连接中创建的表.基于这个想法,也许我应该尝试连接安装文件并以某种方式重新使用该连接(例如,使用mysql2连接池).但是我的感觉告诉我这确实是一个Promise问题,在Jest尝试继续测试执行之前,我需要弄清楚如何在安装文件中完成数据库初始化.

I briefly thought that using two connections might be a problem, in case tables created in one connection cannot be seen until that connection is closed. Building on that idea, maybe I should try connecting in the setup file and re-using that connection in some manner (e.g. by using mysql2 connection pooling). However my senses tell me that this is really a Promise issue, and I need to work out how to finish my db init in the setup file before Jest tries to move on to test execution.

接下来我可以尝试什么?如果这是一种更好的方法,我可以放下mysql2/promise并回到mysql,但我宁愿坚持(并且完全不屑一顾)诺言.

What can I try next? I am amenable to dropping mysql2/promise and falling back to mysql if that is a better approach, but I'd rather persist with (and fully grok) promises if at all possible.

推荐答案

我对此有解决方案.我还不了解Jest的微妙之处,所以我不知道自己是否找到了.

I have a solution to this. I am not yet au fait with the subtleties of Jest, and I wonder if I have just found one.

我的感觉是,由于没有从引导程序到Jest的返回值,因此无法通知它在进行测试之前需要等待promise解析.结果是,在等待测试的过程中诺言就被兑现了,这绝对是一团糟.

My sense is that since there is no return value from the bootstrap to Jest, there is no way to notify to it that it needs to wait for promises to resolve before moving onto the tests. The result of that is that the promises are resolving during awaiting in the tests, which produces an absolute mess.

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

In other words, the bootstrap script can only be used for synchronous calls.

一种解决方案是将可伸缩链从引导文件移动到新的beforeAll()挂钩.我将connect方法转换为返回Promise,因此它的行为与其他方法类似,尤其是在新钩子和现有钩子中,我都return修改了Promise链的值.我相信这可以通知杰斯特(Jest),在其他事情发生之前,诺言必须解决.

One solution is to move the thenable chain from the bootstrap file to a new beforeAll() hook. I converted the connect method to return a Promise, so it behaves like the other methods, and notably I have returned the value of the Promise chain in both the new hook and the existing one. I believe this notifies Jest that the promise needs to resolve before other things can happen.

这是新的测试文件:

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

});

实际上,由于不需要关闭并重新打开连接,因此可以进一步简化.

In fact that can probably be simplified further, since the connection does not need to be closed and reopened.

这是TestDatabase类中connect的非异步版本,可以进行上述更改:

Here is the non-async version of connect in the TestDatabase class to go with the above changes:

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

此解决方案的缺点是:

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

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

A rather more obvious solution is that I can put the database init code into a completely separate process, and then modify the package.json settings:

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

我还没有尝试过,但是我很确定这是可行的-即使init代码是JavaScript,它也必须在退出之前完成所有异步工作.

I have not tried that, but I am pretty sure that would work - even if the init code is JavaScript, it would have to finish all its async work before exiting.

这篇关于如何确保Jest bootstrap文件首先运行?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆