Cypress分离基础设施与测试时自定义task无法使用
尝试将测试基础设施与被测项目分离,在被测项目外部的公共plugins目录中新增名为queryDb的自定义task,已完成代码编译,且在项目配置文件中正确配置了该plugins文件的路径,当前公共plugins文件内的其他插件可正常运行。
执行测试时Cypress抛出如下错误:
"value": "CypressError: `cy.task('queryDb')` failed with the following error:\n\nThe task 'queryDb' was not handled in the plugins file. The following tasks are registered: log\n\nFix this in your plugins file here:\n./../testilize/cypress/plugins/index.ts\n at ...
错误提示plugins文件中仅注册了log任务,未识别到queryDb任务。
当前项目配置继承自被测项目外部的基础配置文件,配置内容如下:
{ "extends": "./../testilize/cypress.json", "baseUrl": "https://www.blabla.com/", "env": { "client": "https://www.blabla.com/", "server": "https://www.blabla.com/" }, "pluginsFile": "./../testilize/cypress/plugins/index.ts", "supportFile": "./../testilize/cypress/support/index.js", "fixturesFolder": "e2e-tests/fixtures", "integrationFolder": "e2e-tests/test-files" }
公共plugins文件(./../testilize/cypress/plugins/index.ts)代码如下:
// cypress/plugins/index.ts /// <reference types="cypress" /> /** * @type {Cypress.PluginConfig} */ const preprocess = require('./preprocess'); const deepmerge = require('deepmerge') const path = require('path'); require('dotenv').config({ path: './../testilize/.env' , override: true }) import { my_connection } from '../support/db-handlers/connections'; function queryTestDb(query, config) { // start connection to db my_connection.connect(); // exec query + disconnect to db as a Promise return new Promise((resolve, reject) => { my_connection.query(query, (error, results) => { if (error) reject(error); else { my_connection.end(); // console.log(results) return resolve(results); } }); }); } module.exports = (on, config) => { require('cypress-log-to-output').install(on) on('task', { log (message) { console.log(message) return true } }) const configJson = require(config.configFile) if (configJson.extends) { const baseConfigFilename = path.join(config.projectRoot, configJson.extends) const baseConfig = require(baseConfigFilename) console.log('merging %s with %s', baseConfigFilename, config.configFile) configJson.env.my_db_name = process.env.my_DB_NAME; configJson.env.my_db_host = process.env.my_DB_HOST; configJson.env.my_db_user = process.env.my_DB_USER; configJson.env.my_db_password = process.env.my_DB_PASSWORD; configJson.env.my_db_port = process.env.my_DB_PORT; return deepmerge(baseConfig, configJson); } on("file:preprocessor", preprocess); on('before:browser:launch', (browser , launchOptions) => { if (browser.name === 'chrome' && browser.isHeadless) { launchOptions.args.push('--disable-gpu', '--no-sandbox', '--disable-dev-shm-usage', '--window-size=1920,1080'); return launchOptions } }) // Usage: cy.task('queryDb', query) on('task', { 'queryDb': query => { return queryTestDb(query, config); } }); return configJson }
测试用例代码如下:
/// <reference types="./../../../testilize/node_modules/cypress" /> let allProjectIDs: any = []; describe('Tests', () => { it('send graphQL request for internal api', () => { cy.task( 'queryDb', `SELECT project_id FROM table_name LIMIT 100;` ).then(res => { console.log(res); allProjectIDs.push(res); console.log(allProjectIDs); }); }); });
- TypeScript 4.6
- Node 14x
- Cypress 9.6
插件导出函数存在提前返回的逻辑分支,导致queryDb的注册代码从未执行:
函数运行时首先完成cypress-log-to-output安装、log任务注册,之后读取配置判断存在extends字段时,直接执行return deepmerge(baseConfig, configJson)退出函数。Cypress的插件导出函数一旦返回,后续所有事件监听、任务注册逻辑都不会运行,因此后续写的文件预处理器注册、浏览器启动钩子、queryDb任务注册的代码全部被跳过,最终Cypress只能识别到最早注册的log任务。
额外注意:Cypress 9.x中多次调用on('task', 任务对象)不会自动合并已注册的任务,后一次传入的任务对象会覆盖前一次的注册内容,原有写法即使去掉提前返回,也会出现log任务被queryDb覆盖的问题。
调整插件函数的执行顺序,遵循「先处理配置合并,再统一注册所有事件/任务,最后返回配置」的结构:
- 把配置合并、环境变量注入的逻辑移到函数最前部,先拿到最终的完整配置对象,不要在注册流程中间插入提前返回的分支
- 合并所有task到同一个
on('task', {})调用中,避免注册被覆盖 - 统一注册预处理器、浏览器启动钩子等其他事件
- 最后返回合并完成的配置对象
调整后的插件核心结构参考:
module.exports = (on, config) => { // 1. 最先处理配置合并 const configJson = require(config.configFile) let finalConfig = configJson if (configJson.extends) { const baseConfigFilename = path.join(config.projectRoot, configJson.extends) const baseConfig = require(baseConfigFilename) console.log('merging %s with %s', baseConfigFilename, config.configFile) configJson.env.my_db_name = process.env.my_DB_NAME; configJson.env.my_db_host = process.env.my_DB_HOST; configJson.env.my_db_user = process.env.my_DB_USER; configJson.env.my_db_password = process.env.my_DB_PASSWORD; configJson.env.my_db_port = process.env.my_DB_PORT; finalConfig = deepmerge(baseConfig, configJson); } // 2. 再注册所有插件、事件、任务 require('cypress-log-to-output').install(on) on("file:preprocessor", preprocess); on('before:browser:launch', (browser , launchOptions) => { if (browser.name === 'chrome' && browser.isHeadless) { launchOptions.args.push('--disable-gpu', '--no-sandbox', '--disable-dev-shm-usage', '--window-size=1920,1080'); return launchOptions } }) // 所有task统一注册,避免覆盖 on('task', { log (message) { console.log(message) return true }, 'queryDb': query => { return queryTestDb(query, finalConfig); } }); // 3. 最后返回配置 return finalConfig }
内容的提问来源于stack exchange,提问作者Idan QA

