前端-端到端自动化测试工具CodeceptJS介绍及使用

975 阅读14分钟

概述

CodeceptJS 是一个基于 Node.js的测试框架,用于进行端到端测试(End-to-End Testing)。它将浏览器交互抽象为简单直观的步骤,并可以在执行时通过传入--step参数输出它们,下面是一个简单的步骤示例。

search --
  test something
    I am on page "https://baidu.com"
    I wait for element {"css":"input[id=kw]"}
    I fill field {"css":"input[id=kw]"}, "ghost"
    I click {"css":"input[type=submit]"}
    I see "百度为您找到以下结果"
  ✔ OK in 9876ms

可以看出每个步骤都是一个和浏览器交互的简单描述,十分易懂,同时如果哪一步失败了也会显示出来,因此问题定位也会极为迅速。

架构

CodeceptJS会将实际执行的指令委托给helper执行,每个helper都针对于CodeceptJS指定的接口有一套自己的实现,因此测试从一个helper迁移到另一个helper是非常容易的,但是需要注意的是由于每个helper的实现细节不同,测试文件可能会在迁移后无法在新的helper上顺利编译和执行,比如WebDriver不支持设置请求头,但是PlaywrightPuppeteer支持。

下图来自于官网。 codeceptjs-architecture.png

CodeceptJS 默认使用 Playwright 来在浏览器中运行测试,但也可以通过 WebDriverPuppeteer 或 Appium 执行测试。

CodeceptJS的安装及简单使用

本文使用Puppeteer作为helper来对CodeceptJS进行安装和使用

关于Puppeteer

Puppeteer(Puppeteer,即木偶,意为在其控制下的Chrome/Chromium浏览器宛如木偶) 是一个 Node.js 库,它提供了一个高级的 API 来通过浏览器开发工具协议(DevTools Protocol) 控制Chrome/Chromium,比如用来生成页面的截图和PDF、爬取单页应用并生成预渲染内容、自动化表单提交、UI测试、键盘输入等。Puppeteer默认以headless(无头模式,即没有UI的模式)下运行,也可以配置成有UI的模式,在CodeceptJS的配置文件codecept.conf.js中可以进行相应的位置,后续会提及细节。

安装
mkdir ui-automation && cd ui-automation
npm init // 根据需要创建出一个package.json文件,或者直接加上-y参数创建出一个默认的package.json文件
npm install codeceptjs puppeteer --save
npx codeceptjs init // 初始化codeceptjs,主要是创建出一些常用文件,比如配置文件和steps_file文件,这里选择helper的时候选择puppeteer即可

这个时候的目录结构大致是这样的:

ui-automation
├── baidu-search_test.js
├── codecept.conf.js
├── jsconfig.json
├── node_modules
├── output
├── package-lock.json
├── package.json
├── steps.d.ts
└── steps_file.js

其中baidu-search_test.js是测试用例文件,codecept.conf.js是codeceptjs的配置文件,output是存放测试报告的目录,steps_file.js则是可以配置自定义的测试步骤。

编写测试用例并运行

baidu-search_test.js

const constants = require('./constants')

Feature('search');

Scenario('test something',  ({ I }) => {
  I.amOnPage('https://baidu.com');
  I.waitForElement(constants.INPUT_BAIDU_SEARCH);
  I.fillField(constants.INPUT_BAIDU_SEARCH, 'ghost');
  I.click(constants.SUBMIT);
  I.see('百度为您找到以下结果');
});

可以看出测试用例的代码很是直白易懂,每个step的名字就表明了每一个用户的实际操作步骤。

constants.js

module.exports = {
  INPUT_BAIDU_SEARCH: { css: 'input[id=kw]' },
  SEARCH_RESULT: { css: 'div[id=head_wrapper]' },
  SUBMIT: { css: 'input[type=submit]' }
}

package.json中添加如下脚本:

"codeceptjs": "codeceptjs run --steps",
"codeceptjs:headless": "HEADLESS=true codeceptjs run --steps",

运行测试用例:

 npm run codeceptjs

自动化测试过程:

Untitled.gif

关于测试用例的编写

Feature && Scenario
const constants = require('./constants')

Feature('search');

Scenario('test something',  ({ I }) => {
  I.amOnPage('https://baidu.com');
  I.waitForElement(constants.INPUT_BAIDU_SEARCH);
  I.fillField(constants.INPUT_BAIDU_SEARCH, 'ghost');
  I.click(constants.SUBMIT);
  I.see('百度为您找到以下结果');
});

测试文件中有两个比较重要的概念,一是Feature,而是Scenario。其含义正如其名,特性和场景,一般来说一个测试文件只有一个Feature和多个Scenario,每个测试用例都包含在一个Scenario中,我们可以通过Scenario接口中的回调函数的I对象执行各种行为,I对象实际上是具体使用的helper的一个代理对象,所以可以通过其控制浏览器。

行为

CodeceptJS支持很多种行为,比如上述的amOnPage(导航到执行页面),click(点击)等,下面是一些常用的行为。

  • amOnPage - 打开一个页面
  • click - 点击指定元素
  • fillField - 输入,如填写一个输入框
  • selectOptioncheckOption - 选择下拉列表中的某个元素,选中单选框或者复选框
  • wait* 等待页面中渲染出某个元素,比较常用的是等待某个按钮出现在页面时,再去点击它
  • grab* 获取某个元素的值,如获取某个label显示出来的文本
  • seedontSee - 检查页面中是否有某个文本
  • seeElementdontSeeElement - 检查页面中是否有某个元素
元素定位

执行行为前需要先找到对应的元素,CodeceptJS支持多种方式去定位元素,一般常用且性能较好的有以下两种。

I.seeElement('.user'); // 通过css选择器定位元素
I.seeElement('//button[contains(., "press me")]'); // 通过xpath

CodeceptJS可以推导出使用的定位方式是哪一种,比如对于.#开头的,会被推测为是css选择器的定位方式,对于//开头的则会推断为是使用了xpath。有些情况下,并不能完全确定是哪一种,则可以使用一种所谓 严格定位(strict locator)的方式,如下所示。

I.seeElement({css: 'div.user'});
I.seeElement({xpath: '//div[@class=user]'});
// 使用严格定位,还可以根据元素名去匹配
I.seeElement({name: 'password'});

通过使用严格定位的定位方式,可以更快的寻找到目标元素,防止codeceptjs按照自己的搜索方式挨个去试。可以看到编写合理的定位元素的方式还是比较重要的。 需要注意的是,CodeceptJS默认只会对找到的第一个元素去执行指定的行为,如果想对所有的元素都执行该行为,可以使用eachElement插件。下方是一个官方使用此插件的例子。

// this example works with Playwright and Puppeteer helper
await eachElement('click all checkboxes', 'form input[type=checkbox]', async (el) => {
  await el.click();
});

测试报告

CodeceptJS支持多种报告,默认是以命令行输出。

search --
  test something
    I am on page "https://baidu.com"
    I wait for element {"css":"input[id=kw]"}
    I fill field {"css":"input[id=kw]"}, "ghost"
    I click {"css":"input[type=submit]"}
    I see "百度为您找到以下结果"
  ✔ OK in 4198ms

  test locators @test
    I am on page "file:///Users/test.html"
    I fill field {"css":"input[type=submit]"}, "ghost"
  ✖ FAILED in 2321ms


-- FAILURES:

  1) search
       test locators @test:
     Field "{"css":"input[type=submit]"}" was not found by text|CSS|XPath
      at new ElementNotFound (node_modules/codeceptjs/lib/helper/errors/ElementNotFound.js:15:11)
      at assertElementExists (node_modules/codeceptjs/lib/helper/Puppeteer.js:2737:11)
      at Puppeteer.fillField (node_modules/codeceptjs/lib/helper/Puppeteer.js:1360:5)
  
  Scenario Steps:
  - I.fillField({"css":"input[type=submit]"}, "ghost") at Test.<anonymous> (./search_test.js:15:5)
  - I.amOnPage("file:///Users/test.html") at Test.<anonymous> (./search_test.js:14:5)

  FAIL  | 1 passed, 1 failed   

此种方式显然不太直观,笔者常用的是mochawesome,这种报告可以以网页的形式展示出测试的结果,包含针对测试用例总数,成功,失败,执行用时的相关统计,并且可以显示出失败的测试用例的失败时那一步的页面截图,对于快速且直观地定位问题还是很有帮助的。

mochawesome生成的报告如下所示。

截屏2024-04-14 22.18.59.png

由于命令行输出报告的形式,通过添加debugverbose参数可以提供非常详细的信息,所以笔者一般会同时使用命令行输出方式以及mochawesome,我们可以通过mocha-multi reporter 配置多种测试报告。可以在codecept.conf.js中添加如下配置:

helpers: {
  Puppeteer: {
    url: 'http://localhost',
    show: true, // 是否使用浏览器窗口运行测试
    windowSize: '1200x900',
    waitForAction: 1000,
  },
  Mochawesome: {
    "uniqueScreenshotNames": "true" // 测试用例失败时生成截图
  }
},
mocha: {
  reporterOptions: {
    "codeceptjs-cli-reporter": {
      stdout: "-",
      options: {
        verbose: false,
        steps: true,
      }
    },
    mochawesome: {
      stdout: "./output/console.log",
      options: {
        reportDir: "./output",
        reportFilename: "report"
      }
    },
  }
},

运行如下命令,以同时使用上述两种形式的报告。

npm run codeceptjs -- --reporter mocha-multi

重试

CodeceptJS提供了全面且强大的重试机制,以提高测试用例的稳定性。

  • 自动重试

    需要用到retryFailedStep插件,会在关于插件章节中详细阐述,该插件可以在每个步骤失败时自动进行重试。

  • 单个step级别的重试

    I.retry(3).see('Welcome');
    

    使用retry方法可以针对每个step进行重试。

  • 多个step级别的重试

    需要使用retryTo插件,会在关于插件章节中详细阐述,该插件可以让每一个块中的步骤时都进行重试。

    // retry these steps 5 times before failing
    await retryTo((tryNum) => {
    I.switchTo('#editor frame');
    I.click('Open');
    I.see('Opened')
    }, 5);
    
  • Scenario级别的重试

Scenario('Really complex', ({ I }) => {
 // test goes here
}).retry(2);
  • Feature级别的重试
Feature('Complex JS Stuff').retry(3);

跳过测试

可以使用 x 和 only 去跳过测试或者单独执行某个测试.

  • xScenario - 跳过当前Scenario
  • Scenario.skip - 跳过当前Scenario
  • Scenario.only - 只执行当前Scenario
  • xFeature - 2.6.6 版本之后可以直接跳过整个Feature
  • Feature.skip - 2.6.6 版本之后可以直接跳过整个Feature
  • Scenario.todo 跳过当前Scenario,并输出 "Test not implemented!" 提示。

关于插件

CodeceptJS支持使用插件,我们可以通过在codecept.conf.js添加类似如下配置使用插件:

plugins: {
  pluginName: {
    xxx: xxx
  }
}

下面介绍几种常用的插件。

  • 自动重试插件 retryFailedStep

    功能:正如上一个章节所说,该插件可以在每个步骤失败时自动进行重试。

    配置:

    plugins: {
        retryFailedStep: {
           enabled: true
        }
    }
    

    常用选项:

    • retries - 重试次数,默认是3次
    • minTimeout - 第一次重试前需要等待的时间,单位为毫秒,默认为1000毫秒,即1秒。
    • maxTimeout - 两次重试之间最大的时间间隔,默认为无限大,实际间隔取决于网络延时,执行步骤的速度等。
    • defaultIgnoredSteps - 重试时忽略的步骤,比如 amOnPage,wait* 等。
  • 多步骤级别的重试插件 retryTo

    功能:正如上一个章节所说,该插件可以让retryTo块中的每个步骤失败时都进行重试。

    配置:

    plugins: {
      retryTo: {
        enabled: true
      }
    }
    

    使用示例:

    // retry these steps 5 times before failing
    await retryTo((tryNum) => {
    I.switchTo('#editor frame');
    I.click('Open');
    I.see('Opened')
    }, 5);
    
  • 自动延时插件 autoDelay

    功能:

    当你点击按钮但没有反应时,可能是因为 JavaScript 事件尚未绑定到该按钮。另外,如果你填写字段并且输入验证不接受你的输入,可能是因为你输入得太快了。而这个插件允许在测试执行过快时减慢测试的执行速度。它会在执行操作命令之前和之后添加一个小的延迟(默认值分别是:100ms/200ms),以避免上述这种情况的发生。

    默认情况下,受影响的命令有:

    • click
    • fillField
    • checkOption
    • pressKey
    • doubleClick
    • rightClick

    配置:

    plugins: {
       autoDelay: {
         enabled: true
       }
    }
    

CodeceptJS常用配置详解

exports.config = {
  helpers: {
    // helper相关的配置
  },
  plugins: {
    // 插件
  },
  include: {
    // actor及其对应的文件配置,比如最常用的是 I: './src/steps_file.js'
  },
  // 可用来存放测试报告或者失败的测试的截图等
  output: './output',
  // 指定测试文件,支持通配符
  tests: './src/*.spec.js',
  // 测试超时时间,超过设置值会直接被终止运行。
  timeout: xxx,
}

CodeceptJS X Puppeteer的建议配置

helpers: {
  Puppeteer: {
    url: 'http://localhost',
    show: true, // 是否使用浏览器窗口运行测试(headful/headless)
    windowSize: '1200x900',
    // 在点击或者按键操作之后需要等待的时间,单位时毫秒,默认是100毫秒。
    // 这个配置可以防止点击操作后应用还没来得及响应就执行下一个步骤引发的错误。
    waitForAction: 1000, 
    // 在执行多个测试之间是否重启浏览器,一般情况下没有必要重启,重启浏览器执行测试还是挺耗费时间的
    restart: false,
    // 在执行多个测试之间不重启浏览器时是否保存浏览器状态,建议保存
    keepBrowserState: true, 
    // 在执行多个测试之间不重启浏览器时是否保存cookies,建议保存
    keepCookies: true,
    // 执行wait*步骤时默认等待的最大时间,单位为毫秒,可以根据应用本身情况去设置。
    waitForTimeout: 30000,
    // 何时认为页面跳转已完成,配合 I.waitForNavigation 这个step使用,避免测试时在页面跳转或者路由
    // 切换完成前就开始执行步骤。
    // 有四种值可以配置,也支持同时配置多个值
    // 1.load load事件发生时,即页面所有资源加载完毕 
    // 2.domcontentloaded DOMContentLoaded事件发生时,即HTML文档完全解析且所有延迟脚本
    //(`<script defer src="...">` 和 `<script type="module">`)下载并执行完毕后触发
    // 3.networkidle0 500毫秒内网络连接数达到零,该配置对SPA(单页面程序)来说更为有用,
    // SPA通过路由切换完成动态更新后,上述两个事件不会再触发,可以通过一段时间内没有新的网络请求来判断
    // 出已经切换到新的路由了。
    // 4.networkidle2 500毫秒内网络连接数不超过2个。
    waitForNavigation: [
      'load',
      'networkidle0',
    ],
    // 配置用于设置按键之间的延迟时间,以模拟用户在输入时的速度。
    // 这对于模拟用户在输入表单字段时的实际速度很有用,以避免输入过快而导致应用程序无法处理
    pressKeyDelay: 50,
  },
},

自定义helper

有时候CodeceptJS提供的Helper的api接口无法满足业务需要,比如让页面前进或者后退(这个puppeteer是直接支持的),虽然可以通过类似

I.executeScript("window.history.back();");

的方法达成使页面后退的目的,但终归不是那么优雅。此外如果想要在测试执行的某个特定的时间去完成一些事情也是需要通过自定义helper来完成的。

自定义helper的大致方法如下所示:

const Helper = require('@codeceptjs/helper');

class MyHelper extends Helper {

  // before/after hooks
  _before() {
    // remove if not used
  }

  _after() {
    // remove if not used
  }

  // 自定义函数
  // 可以通过 this.helpers['helperName'] 访问其他helper,比如使用this.helpers['Puppeteer']
  // 访问Puppeteer

}

module.exports = MyHelper;

注意:

  1. 自定义helper的类必须继承 @codeceptjs/helper
  2. 带下划线的会被视为内部函数,是无法通过I对象访问到的。
  3. 其他自定义函数都可以在I对象访问到。

以下是一个支持页面前进后退以及在Feature执行后生成HAR文件的范例:

const Helper = require('@codeceptjs/helper');
const PuppeteerHar = require('puppeteer-har');
  
class PuppeteerHelper extends Helper {
  
  getPage(){
    return this.helpers.Puppeteer.page;
  }
  
  goBack(){
    this.getPage().goBack();
  }
  
  goForward(){
    this.getPage().goForward();
  }
  
  // 钩子函数,会在每个`Feature`执行前执行
  async _beforeSuite(suite){
    this.har = new PuppeteerHar(this.getPage());
    await this.har.start({
      path: `./output/${suite.title}-automation-test-results.har`,
      saveResponse: true,
    })
  }

  // 钩子函数,会在每个`Feature`执行后执行
  async _afterSuite(suite){
    await this.har.stop();
  }
}

module.exports = PuppeteerHelper;

然后我们需要在codecept.conf.js中添加如下配置:

helpers: {
  Puppeteer: {
    url: 'http://localhost',
    show: true,
    windowSize: '1200x900',
    waitForAction: 3000,
    // 注意一下两个选项必须按如下值设定,否则在执行
    restart: false,
    keepBrowserState: true,
  },
  PuppeteerHelper: {
    require: './puppeteer_helper.js',
  }
},

这里值得注意的是上述两个配置:

 restart: false,
 keepBrowserState: true,

必须按如上值进行设定,否则在执行_beforeSuite函数时,Puppeteer.pageundefined,会有如下报错。

1) search
       "before all" hook: codeceptjs.beforeSuite for "test something":
     Cannot read properties of undefined (reading 'mainFrame')
      at new PuppeteerHar (node_modules/puppeteer-har/lib/PuppeteerHar.js:31:36)
      at PuppeteerHelper._beforeSuite (puppeteer_helper.js:20:16)
      at /Users/penghao/codecept-test/node_modules/codeceptjs/lib/listener/helpers.js:26:69
  
  
2) search
     "after all" hook: codeceptjs.afterSuite for "test locators @test":
     Cannot read properties of undefined (reading 'stop')
     at PuppeteerHelper._afterSuite (puppeteer_helper.js:29:20)
     at /Users/penghao/codecept-test/node_modules/codeceptjs/lib/listener/helpers.js:26:69

猜测是在执行_beforeSuite这个钩子函数的时候,还没有page,所以那个时候它的值是空的,加上keepBrowserState就没问题了,restart也必须设置成false,否则keepBrowserState的值会被忽略。

补充:HAR文件是什么?

HAR(HTTP Archive format)是一种用于记录Web浏览器与站点交互的JSON格式日志。它记录了发送的所有请求和收到的响应,以及加载资源(如HTML、CSS、JavaScript文件和图像)所花费的时间,还包括有关报头、正文内容和服务器端点的详细信息。HAR文件对于了解Web性能和识别问题至关重要。它们是Web开发人员和性能分析师的宝贵工具,用于分析Web应用程序的性能、调试问题以及优化资源加载策略。

常用技巧

  1. 可以通过标签配合grep参数去执行指定的测试用例。

    Feature('demo');
    Scenario('test locators @test',  ({ I }) => {
      I.amOnPage('file:///Users/test.html');
      I.fillField({ css: 'input[type=email]' }, 'ghost');
    });
    

    运行

    npx codeceptjs run --grep @test 
    

    即只会执行这一个Scenario,当然也可以把标签加在Feature上,如此可以只执行这一个测试文件的用例。

    标签也可以用来将不同的测试用例跑在不同的环境上或者对它们进行分类。

  2. 关于debug

    我们可以使用pause函数来暂停执行测试。

     Scenario('test something',  ({ I }) => {
     I.amOnPage('https://baidu.com');
     I.waitForElement(constants.INPUT_BAIDU_SEARCH);
     I.fillField(constants.INPUT_BAIDU_SEARCH, 'ghost');
     I.click(constants.SUBMIT);
     I.see('百度为您找到以下结果');
     I.goBack();
     pause();
     I.goForward();
    });
    

    CodeceptJS这个pause函数强大的地方在于当暂停执行测试以后,还会在终端出现一个交互的shell,

      Interactive shell started
      Use JavaScript syntax to try steps in action
      - Press ENTER to run the next step
      - Press TAB twice to see all available commands
     - Type exit + Enter to exit the interactive shell
     - Prefix => to run js commands 
     Enable OpenAI assistant by setting OPENAI_API_KEY env variable
     I.
        I go forward 
     I.goBack()
        I go back 
     I.goForward();
        I go forward 
     I.
    

    通过按回车键可以实现类似单步调试的效果,并且我们也可以直接在命令行中输入步骤,这些步骤会立即被执行。 这样我们就可以在失败的那一个步骤前加上pause函数,然后检查UI是否有异常了。