35分钟阅读时间 (6909字)

使用Joomla!和Cypress进行端到端测试 - 我的第一步和思考

2023---JCM-End-to-end-testing-with-Cypress1cypress

自动化测试并不是大型项目中软件开发者的专用工具。尤其是对于扩展,自动化测试有助于快速识别问题。它们有助于确保扩展在新版本的Joomla中能正常运行。

Joomla核心开发者希望第三方软件开发者测试他们的扩展,以便在用户发现之前找到错误。这需要大量的时间和无聊的工作。因此,这通常不会做。尤其是当需要人为地对每个新版本进行手动测试时。自动化测试使得在每次发布时无需人类执行这些步骤即可重复手动步骤。这样,在用户访问实时系统之前,就能找到错误。

顺便说一句,任何想要检查Cypress的人都会觉得这篇文章是一个很好的起点。你可以测试问题,而无需自己安装和配置一切。在Joomla项目的GitHub仓库中,一切准备就绪。

简介

"虽然质量不能在测试中得到验证,但同样明显的是,没有测试,就不可能开发出任何高质量的东西。" – [James A. Whittaker]

在我第一次遇到Cypress之前,我无法想象那些经常阻碍我测试的障碍实际上被部分移除。我花了大量时间测试软件——而且以前甚至更多时间处理由于缺乏测试而产生的问题!现在,我坚信,尽可能接近编程时间的测试、自动化的、频繁的——理想情况下在每次程序更改后——带来的效益超过其成本。更重要的是:测试甚至可以很有趣。

  • 尽可能接近编程时间
  • 自动化的
  • 频繁的——理想情况下在每次程序更改后

    带来的效益超过其成本

学习测试方法值得!测试方法具有持久性,因为它们不仅可以用任何编程语言,而且可以应用于几乎任何人类工作。你应该时不时地测试生活中几乎所有的 重要事物。测试方法与特定软件工具无关。与编程技术或编程语言不同,它们常常时兴时衰,如何设置良好测试的知识是永恒的。

谁应该阅读这篇文章?

认为软件测试是浪费时间的人应该看看这篇文章。特别是,我想邀请那些一直想为他们的软件编写测试但从未实现过的开发者阅读这篇文章。Cypress可能是一个消除这些障碍的方法。

一些理论

魔法三角形

魔法三角形描述了成本、所需时间和可达到质量之间的关系。最初,这种关系是在项目管理中被认识和描述的。然而,你可能也在其他领域听说过这种紧张关系。它几乎是公司几乎所有运营过程中一个重要的问题。

例如,人们普遍认为,更高的成本对质量和/或完成日期有积极影响——也就是说,时间。

 

The Magic Triangle in Project Management - If more money is invested in the project, this has a positiv impact on quality or time.

反过来,成本节约将迫使质量下降和/或完成日期延迟。

The Magic Triangle in Project Management - If less money is invested in the project, this has a negative impact on quality or time.

现在,魔法就出现了:我们克服了时间、成本和质量之间的相关性!因为,从长远来看,这实际上是可以克服的。

从长远来看,可以克服时间、成本和质量之间的联系。

也许你也曾在实践中体验到,质量下降并不会在长期内节省成本。由此产生的技术债务往往甚至会导致成本增加和额外时间。

In the long run, the correlation between cost, time and quality can actually be overcome.

技术债务指的是对不太好的编程软件进行更改和改进时所需的额外工作量,与编写良好的软件相比。Martin Fowler区分了以下类型的技术债务:那些故意进入的和那些意外进入的。他还区分了谨慎的技术债务和鲁莽的技术债务。

Technical debt

成本与收益

在文献中,你可以找到关于软件项目成功机会的令人震惊的统计数据。与20世纪90年代A.W. Feyhl的研究中记录的负面图景相比,几乎没有改变。在这里,对50个组织中的162个项目进行分析,确定了与原始计划相比的成本偏差:70%的项目显示至少有50%的成本偏差!有些事情是不正确的!你能接受这一点吗?

一个解决方案就是完全放弃成本估算,并遵循#NoEstimates运动的论点。这个运动认为,在软件项目中,成本估算是没有意义的。根据#NoEstimates的观点,软件项目总是包含生产新事物的内容。新事物与已经存在的经验不可比,因此是不可预测的。

随着经验的增加,我越来越得出结论,极端的观点并不好。解决方案几乎总是在中间。在软件项目中也要避免极端,寻找中间点。我认为,你不需要有一个100%确定的计划。但你也不应该天真地开始一个新项目。尽管软件项目管理,尤其是成本估算是一个重要的话题,但我不会在这个文本中再继续困扰你。本文的重点是展示如何将端到端测试集成到软件开发的实际工作流程中。

将软件测试集成到您的流程中

您已决定测试您的软件。太好了!何时进行测试最佳?让我们看看在不同项目阶段修复一个错误所需的成本。您发现错误越早,修复它的成本就越低。

Relative costs for troubleshooting in various project stages

测试和调试:有些词经常被放在一起说,因此它们的含义被等同起来。然而,仔细观察后,这些术语代表不同的解释。测试和调试属于这些词。这两个术语的共同点是它们都能检测到故障。但在意义上也有差异。

  • 测试在开发过程中发现未知故障。发现故障是昂贵的,而定位和消除错误是便宜的。另一方面,便宜。
  • 调试器修复产品完成后发现的故障。发现故障是免费的,但定位和修复错误是昂贵的。

结论:尽早开始集成测试最有意义。不幸的是,这在以Joomla为代表的开源项目中很难实施,因为它们主要依靠志愿者。

持续集成(CI)
测试持续集成

想象以下场景。一个流行的内容管理系统的新版本即将发布。自上次发布以来,团队中的开发人员所贡献的一切现在首次被一起使用。紧张感正在上升!一切都会正常工作吗?如果项目集成了测试,所有测试都会成功吗?或者,新版本的发布是否需要再次推迟,并且前面将迎来令人紧张的故障修复数小时?顺便说一句,推迟发布日期也不利于软件产品的形象!没有开发者喜欢经历这种情况。更好的是,您随时都知道软件项目目前的状态?不符合现有代码的代码应该在它们被“调整”后才进行集成。尤其是在必须修复安全漏洞越来越普遍的时代,项目始终应该能够创建一个版本!这就是持续集成发挥作用的地方。

在持续集成中,软件的各个元素永久集成。软件以小周期的方式创建和测试。这样,您在早期就能在集成过程中遇到问题或失败的测试,而不是几天或几周后。通过连续集成,故障排除变得更容易,因为错误是在编程时发现的,通常只会影响程序的一小部分。Joomla使用持续集成集成新代码。只有当所有测试通过时,新代码才会被集成。

通过持续集成新软件,故障排除变得更容易,因为错误是在编程时发现的,通常只会影响程序的一小部分。

为了确保在持续集成过程中始终有所有程序部分的测试可用,您应该开发测试驱动型软件。

测试驱动开发(TDD)

测试驱动开发是一种编程技术,它使用小步骤的开发。首先编写测试代码。然后,您才创建要测试的程序代码。对程序的任何更改都只能在为该更改创建测试代码之后进行。因此,您的测试在创建后会立即失败。所需的功能尚未在程序中实现。然后,您创建实际的程序代码——即满足测试的程序代码。

TDD测试帮助您正确编写程序

当你第一次听到这项技术时,你可能对这个概念感到不自在。毕竟,“人”总是想先做一些有成效的事情。编写测试看起来一开始并不像是件有成效的事情。试试看。有时,你只有在真正了解它之后才会与新技术成为朋友!在高测试覆盖率的项目中,我在添加新功能时感到更自在。

如果你阅读了文本末尾的练习部分,你可以试试。首先创建测试,然后编写Joomla核心代码。然后,将所有内容一起作为PR提交到Github。如果每个人都这样做,Joomla将拥有理想的测试覆盖率。

行为驱动开发(BDD)

BDD不是另一种编程技术或测试技术,而是一种软件开发的最佳实践。BDD的理想应用是与TDD结合使用。原则上,行为驱动开发代表的是测试程序的执行,即程序的行为,而不是程序代码的实现。测试检查规格,即客户需求是否得到满足。

当你以行为驱动的方式开发软件时,测试不仅可以帮助你正确编写程序,还可以帮助你编写正确的程序

我所说的“编写正确的程序”是什么意思:它可能发生的情况是,用户看待事物的角度与开发者不同。Joomla中删除文章的工作流程就是一个例子。我一次又一次地遇到点击回收站中状态图标并感到惊讶的用户。用户通常直觉地认为项目现在已被永久删除,但实际上它只是从回收站切换到了活动状态。对于开发者来说,点击状态图标是状态的变化,一个切换。在其他所有视图中,情况都是这样。为什么回收站中就不同呢?对于开发者来说,功能实现没有错误。Joomla运行正常。但在我的眼里,那个位置的功能并不是正确的,因为大多数用户会以完全不同的方式描述/请求它。

在行为驱动开发中,软件需求通过称为场景或用户故事的例子来描述。行为驱动开发的特点包括

  • 用户在软件开发过程中的积极参与,
  • 以文本形式记录所有项目阶段,通常使用描述语言Gherkin,
  • 自动测试这些用户故事/案例研究,
  • 逐步实施。因此,可以随时访问要实施的软件的描述。借助此描述,您可以持续确保已实现的程序代码的正确性。

Joomla项目在Google Summer of Code项目中引入了BDD。当时希望没有编程知识的用户能够通过Gherkin)更轻松地参与。这种方法没有得到持续跟进。当时,Joomla使用Codeception作为测试工具。现在,使用Cypress,也可以以BDD方式开发BDD开发

规划

测试类型
  • 单元测试:单元测试是一种独立测试最小程序单元的测试。
  • 集成测试:集成测试是一种测试各个单元之间交互的测试。
  • 端到端测试或验收测试:验收测试检查程序是否满足最初定义的任务。
策略

如果你想向Joomla添加新功能并用测试来确保其安全,你可以有两种方法。

自上而下和自下而上是理解与呈现复杂问题的两种基本不同的方法。自上而下是逐步从抽象和一般到具体和具体的。以一个例子来说明:像Joomla这样的内容管理系统通常在浏览器中展示网站。然而,具体来说,这个过程中有多个小任务。其中之一是在标题中显示特定文本的任务。

自下而上描述了相反的方向:此时,值得再次记住,行为驱动开发的一个要素是创建软件行为的文本描述。这种接受标准的描述有助于创建测试——特别是顶层的端到端测试或接受测试。

今天创建测试的常用方法是自下而上。如果你更喜欢行为驱动软件开发,你应该使用相反的策略。你应该使用自上而下的策略。在自上而下的策略中,误解在设计阶段早期就被识别出来了。

Test strategies: Top-down-testing and Bottom-up-testing

  • 自上而下测试:在应用自上而下的策略时,从接受测试开始——即与用户需求最紧密相关的系统部分。对于面向人类用户的软件,这通常是用户界面。重点是测试用户如何与系统交互。自上而下测试的缺点是必须花费大量时间创建测试副本。尚未集成的组件必须由占位符替换。最初没有真正的程序代码。因此,缺失的部分必须人工创建。逐渐,这些人工数据被真实计算的数据所取代。

  • 自下而上测试:如果你遵循自下而上的策略,你从单元测试开始。一开始,开发者有目标状态的概念。然而,他首先将这个目标分解成单个组件。自下而上方法的缺点是难以测试组件在真实情况下的使用情况。自下而上测试的优点是我们可以非常快地完成软件部分。然而,这些部分应该谨慎使用。它们确实是正确的。这是单元测试所保证的。但是,最终结果是否真的是客户想象中的软件,这一点并不能保证。

迈克·科恩的测试金字塔

应该实现多少种测试类型?迈克·科恩的测试金字塔描述了自动化软件测试应用的概念。金字塔由三个层次组成,根据使用频率和相关性进行结构化。

理想情况下,测试金字塔的底部由许多快速且易于维护的单元测试组成。这样,大多数错误都可以快速发现。

中间层是集成测试。它们为针对关键接口的测试提供服务。集成测试的执行时间更长,它们的维护也比单元测试更复杂。

金字塔的顶部由慢速的端到端测试组成,有时需要大量维护。端到端测试对于测试整个应用程序作为系统非常有用。

需求

在以下实践部分工作中,你需要哪些设备?

在以下实践部分工作中,你必须满足哪些要求?你不必满足很多要求就能处理本手册的内容。当然,你必须有一台电脑。安装或可安装在电脑上的Git、NodeJS和Composer的开发环境以及本地Web服务器。

你个人应该具备哪些知识?

你应该了解基本的编程技术。理想情况下,你已经编写过一个小的Web应用程序。无论如何,你应该知道在开发计算机上存储文件的位置以及如何在网络浏览器中加载它们。最重要的是,你应该享受尝试新事物。

试试看。将测试集成到你的下一个项目中。也许你第一次使用测试的经验可以节省你繁琐的调试会话或系统中的尴尬错误。毕竟,有了测试的安全网,你可以更轻松地开发软件。

设置

使用Joomla设置Cypress!

在Github上可用的开发者版本中,Joomla已经为Cypress配置就绪。已经有了一些你可以用作指南的测试。因此,你不需要自己设置一切以获得初步了解。这样,你可以尝试Cypress,了解其优缺点,并自行决定是否要使用测试工具。

设置本地环境的步骤

将存储库克隆到本地Web服务器的根目录

$ git clone https://github.com/joomla/joomla-cms.git

导航到joomla-cms文件夹

$ cd joomla-cms

根据Joomla路线图,下一个主要版本5.0将于2023年10月发布。为了保持最新,我这里使用这个开发版本。

切换到分支5.0-dev

$ git checkout 5.0-dev

安装所有需要的Composer包

$ composer install

安装所有需要的npm包

$ npm install

有关设置工作站的更多信息和方法,请参阅Joomla文档文章《设置Joomla开发工作站》。对于Cypress,有关信息在cypress.io。但在此阶段这不是必需的。Joomla会为你设置一切。你只需要通过配置文件joomla-cms/cypress.config.js设置你的个人数据。

设置你的个人数据。为此,你可以使用模板joomla-cms/cypress.config.dist.js作为参考。在我的情况下,这个文件看起来是这样的

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  fixturesFolder: 'tests/cypress/fixtures',
  videosFolder: 'tests/cypress/output/videos',
  screenshotsFolder: 'tests/cypress/output/screenshots',
  viewportHeight: 1000,
  viewportWidth: 1200,
  e2e: {
    setupNodeEvents(on, config) {},
    baseUrl: 'http://localhost/joomla-cms',
    specPattern: [
      'tests/cypress/integration/install/*.cy.{js,jsx,ts,tsx}',
      'tests/cypress/integration/administrator/**/*.cy.{js,jsx,ts,tsx}',
      'tests/cypress/integration/module/**/*.cy.{js,jsx,ts,tsx}',
      'tests/cypress/integration/site/**/*.cy.{js,jsx,ts,tsx}'
    ],
    supportFile: 'tests/cypress/support/index.js',
    scrollBehavior: 'center',
    browser: 'firefox',
    screenshotOnRunFailure: true,
    video: false
  },
  env: {
    sitename: 'Joomla CMS Test',
    name: 'admin',
    email: This email address is being protected from spambots. You need JavaScript enabled to view it.',
    username: 'admin',
    password: 'adminadminadmin',
    db_type: 'MySQLi',
    db_host: 'mysql',
    db_name: 'test_joomla',
    db_user: 'root',
    db_password: 'root',
    db_prefix: 'j4_',
  },
})

具体来说,我已将目录tests/cypress/integration/module/**/*.cy.{js,jsx,ts,tsx}添加到specPattern数组中,因为我希望将来在那里保存模块测试。然后我更改了用户名和密码,因为我还想手动测试安装并更好地记住自动分配的密码。我使用Docker容器作为数据库。因此,我更改了数据库服务器和访问数据。最后,我必须设置Joomla安装的根URLhttp://localhost/joomla-cms

使用Cypress

通过Web浏览器

通过CLI在Joomla根目录下调用npm run cypress:open。稍后,Cypress应用将打开。我们之前创建的文件joomla-cms/cypress.config.dist.js可以被检测到,这可以从E2E测试被指定为配置的事实中看出。

Cypress App opens after calling 96;npm run cypress:open96;.

在这里,你可以选择是否要运行E2E测试以及你想要使用哪个浏览器。对于示例,我选择了“在Firefox中开始测试”选项。

E2E tests in the Cypress app: select the browser to use.

将列出所有可用的测试套件,你可以点击你想要运行的测试套件。当你选择测试套件时,测试将运行,你可以在浏览器中实时查看测试的运行情况。

Joomla test suite in Firefox via Cypress App.

当测试运行时,你可以在一侧看到执行的脚本,在右侧看到浏览器中的结果。这些不仅仅是截图,而是那一刻浏览器真实的快照,因此你可以看到实际的HTML代码。测试的截图甚至视频也是可能的。

Joomla installation test in progress.

试一试。如果您使用 db_host: 'localhost',,您可以测试安装,从而正确配置Joomla以进行以下文本部分的操作。

如果您像我一样使用外部源(不是localhost;我使用docker容器)作为 db_host,则此类安装的测试尚未准备就绪。在这种情况下,安装过程中存在安全问题,这在测试中尚未考虑。在这种情况下,请使用文件 joomla-cms/cypress.config.js 中输入的信息手动安装Joomla。以下测试将使用此配置文件中的设置,例如用于登录Joomla管理区域。这样,测试开发者不必关心输入登录数据。匹配的用户名和密码始终会自动从配置文件中获取。

无头模式

默认情况下,cypress run 以无头模式运行所有测试。以下命令执行所有已编码的测试,并在出错时将截图保存在目录 /joomla-cms/tests/cypress/output/screenshots 中。输出目录在 cypress.config.js 文件中设置。

$ npm run cypress:run

其他CLI命令

还有一些有用的命令,它们不是在Joomla项目的 package.json 中作为脚本实现的。我通过 npx[docs.npmjs.com/commands/npx] 来执行它们。

cypress verify

cypress verify 命令验证Cypress是否正确安装并可运行。

$ npx cypress verify

✔  Verified Cypress! /.../.cache/Cypress/12.8.1/Cypress
cypress info

cypress info 命令输出有关Cypress和当前环境的信息。

$ npx cypress info
Displaying Cypress info...

Detected 2 browsers installed:

1. Chromium
  - Name: chromium
  - Channel: stable
  - Version: 113.0.5672.126
  - Executable: chromium
  - Profile: /.../snap/chromium/current

2. Firefox
  - Name: firefox
  - Channel: stable
  - Version: 113.0.1
  - Executable: firefox
  - Profile: /.../snap/firefox/current/Cypress/firefox-stable

Note: to run these browsers, pass <name>:<channel> to the '--browser' field

Examples:
- cypress run --browser chromium
- cypress run --browser firefox

Learn More: https://on.cypress.io/launching-browsers

Proxy Settings: none detected
Environment Variables: none detected

Application Data: /.../.config/cypress/cy/development
Browser Profiles: /.../.config/cypress/cy/development/browsers
Binary Caches: /.../.cache/Cypress

Cypress Version: 12.8.1 (stable)
System Platform: linux (Ubuntu - 22.04)
System Memory: 4.08 GB free 788 MB
cypress version

cypress version 命令打印安装的Cypress二进制版本、Cypress包的版本、创建Cypress所使用的Electron版本以及捆绑的node版本。

$ npx cypress version
Cypress package version: 12.8.1
Cypress binary version: 12.8.1
Electron version: 21.0.0
Bundled Node version: 16.16.0

Cypress的 文档 提供更多详细信息。

编写第一个自己的测试

如果到目前为止一切正常,我们可以开始创建自己的测试。

了解概述

从已开发的测试中学习

Joomla CMS的开发版本中已经有Cypress测试。这些位于文件夹 /tests/System/integration。喜欢通过例子学习的人在这里会找到一个合适的介绍。

导入代码以重复任务

Joomla开发者正在开发NodeJs 项目 joomla-cypress,它为常见测试情况提供测试代码。这些代码在安装CMS的开发版本时通过 npm install 导入,通过

  • package.json 和通过
  • 支持文件 /tests/System/support/index.js。支持文件在配置 cypress.config.js 中定义。
// package.json
{
  "name": "joomla",
  "version": "5.0.0",
  "description": "Joomla CMS",
  "license": "GPL-2.0-or-later",
  "repository": {
    "type": "git",
    "url": "https://github.com/joomla/joomla-cms.git"
  },
...
  "devDependencies": {
    ...
    "joomla-cypress": "^0.0.16",
    ...
  }
}

以下代码示例展示了如何模拟点击工具栏按钮。例如,Cypress.Commands.add('clickToolbarButton', clickToolbarButton) 使命令 clickToolbarButton() 可用于自定义测试,并通过 cy.clickToolbarButton('new') 模拟点击按钮 New。所需的代码在下面的代码片段中显示。

// node_modules/joomla-cypress/src/common.js
...
const clickToolbarButton = (button, subselector = null) => {
  cy.log('**Click on a toolbar button**')
  cy.log('Button: ' + button)
  cy.log('Subselector: ' + subselector)

  switch (button.toLowerCase())
  {
    case "new":
      cy.get("#toolbar-new").click()
      break
    case "publish":
      cy.get("#status-group-children-publish").click()
      break
    case "unpublish":
      cy.get("#status-group-children-unpublish").click()
      break
    case "archive":
      cy.get("#status-group-children-archive").click();
      break
    case "check-in":
      cy.get("#status-group-children-checkin").click()
      break
    case "batch":
      cy.get("#status-group-children-batch").click()
      break
    case "rebuild":
      cy.get('#toolbar-refresh button').click()
      break
    case "trash":
      cy.get("#status-group-children-trash").click()
      break
    case "save":
      cy.get("#toolbar-apply").click()
      break
    case "save & close":
      cy.get(".button-save").contains('Save & Close').click()
      break
    case "save & new":
      cy.get("#save-group-children-save-new").click()
      break
    case "cancel":
      cy.get("#toolbar-cancel").click()
      break
    case "options":
      cy.get("#toolbar-options").click()
      break
    case "empty trash":
    case "delete":
      cy.get("#toolbar-delete").click()
      break
    case "feature":
      cy.get("#status-group-children-featured").click()
      break
    case "unfeature":
      cy.get("#status-group-children-unfeatured").click()
      break
    case "action":
      cy.get("#toolbar-status-group").click()
      break
    case "transition":
      cy.get(".button-transition.transition-" + subselector).click()
      break
  }

  cy.log('--Click on a toolbar button--')
}

Cypress.Commands.add('clickToolbarButton', clickToolbarButton)
...

以下代码还展示了如何登录到管理区域。

// /node_modules/joomla-cypress/src/user.js
...
const doAdministratorLogin = (user, password, useSnapshot = true) => {
  cy.log('**Do administrator login**')
  cy.log('User: ' + user)
  cy.log('Password: ' + password)

  cy.visit('administrator/index.php')
  cy.get('#mod-login-username').type(user)
  cy.get('#mod-login-password').type(password)
  cy.get('#btn-login-submit').click()
  cy.get('h1.page-title').should('contain', 'Home Dashboard')

  cy.log('--Do administrator login--')
}

Cypress.Commands.add('doAdministratorLogin', doAdministratorLogin)

...
各个环境中的常见任务

在目录 /tests/System/support 中,您将找到各个环境中的常见任务。为了便于重用,它们通过支持文件 /tests/System/support/index.js 导入。一个常见的任务示例是登录到管理区域,该任务在文件 /tests/System/support/commands.js 中使用 doAdministratorLogin 函数处理。

以下代码还展示了如何在测试中使用 cypress.config.js 配置中的信息。Cypress.env('username') 将分配给 env 组中 username 属性的值。

此外,我们还可以看到如何覆盖命令。Cypress.Commands.overwrite('doAdministratorLogin' ...), 覆盖了我们刚才在 joomla-cypress 包中看到的代码。优势是用户名和密码会自动从各个配置中使用。

// /tests/System/support/commands.js
...
Cypress.Commands.overwrite('doAdministratorLogin', (originalFn, username, password, useSnapshot = true) => {
  // Ensure there are valid credentials
  const user = username ?? Cypress.env('username');
  const pw = password ?? Cypress.env('password');

  // Do normal login when no snapshot should be used
  if (!useSnapshot) {
    // Clear the session data
    Cypress.session.clearAllSavedSessions();

    // Call the normal function
    return originalFn(user, pw);
  }

  // Do login through the session
  return cy.session([user, pw, 'back'], () => originalFn(user, pw), { cacheAcrossSpecs: true });
});
...

安装自己的Joomla扩展

为了了解如何测试您的代码,我们将通过Joomla后台安装一个简单的示例组件。安装文件可以从Codeberg下载。

Install own Joomla extension.

安装完成后,您可以在Joomla后台左侧侧边栏中找到Foo组件的查看链接。

View of the example component in the Joomla backend.

现在我们已经设置了测试环境,并有了测试代码。

第一个自测

钩子

在测试后台时,您会注意到每个测试都必须从登录开始。我们可以通过使用beforeEach()函数来避免这种重复的代码。这个所谓的钩子在每次测试运行之前执行我们输入的代码,因此得名beforeEach()

Cypress提供了几种类型的钩子,包括在测试组中的测试之前或之后运行的beforeafter钩子,以及在每个组内的单个测试之前或之后运行的beforeEachafterEach钩子。钩子可以在全局范围内或特定于某个described块内定义。文件tests/System/integration/administrator/components/com_foos/FoosList.cy.js中的下一个代码示例会在describedtest com_foos features中的每个测试之前执行登录操作。

现在,我们开始实际操作,在编写第一个生产测试之前,创建文件tests/System/integration/administrator/components/com_foos/FoosList.cy.js,并编写以下代码片段。我们的第一个示例应该在任何测试之前成功登录到后端!我们将创建第一个测试后测试这一点。

// tests/System/integration/administrator/components/com_foos/FoosList.cy.js

describe('Test com_foos features', () => {
  beforeEach(() => {
    cy.doAdministratorLogin()
  })

})

注意:在文件/tests/System/support/index.js中实现的钩子适用于测试套件中的每个测试文件。

成功的测试

我们为测试安装的组件包含三个元素AstridNinaElmar。首先,我们测试这些元素是否成功创建。

// tests/System/integration/administrator/components/com_foos/FoosList.cy.js

describe('Test com_foos features', () => {
  beforeEach(() => {
    cy.doAdministratorLogin()
  })

  it('list view shows items', function () {
    cy.visit('administrator/index.php?option=com_foos')

    cy.get('main').should('contain.text', 'Astrid')
    cy.get('main').should('contain.text', 'Nina')
    cy.get('main').should('contain.text', 'Elmar')

    cy.checkForPhpNoticesOrWarnings()
  })
})

注意:在文件/node_modules/joomla-cypress/src/support.js中找到的函数checkForPhpNoticesOrWarnings()

我们通过Cypress命令get获取DOM元素main

您应该在左侧侧边栏中可用的测试列表中找到您刚刚创建的测试FooList.cy.js。如果不是这种情况,请关闭浏览器并再次运行npm run cypress:open

Joomla run test for own extension.

单击测试名称以运行它。它应该成功结束,并且您会看到绿色的消息。

The view after the test has run successfully.

失败的测试

将行cy.get('main').should('contain.text', 'Sami')添加到测试文件,以便测试失败。没有这个名称的元素。在保存测试文件后,Cypress会注意到更改。每次更改后,Cypress都会自动重新运行测试文件中的所有测试。

// tests/System/integration/administrator/components/com_foos/FoosList.cy.js
describe('Test com_foos features', () => {
  beforeEach(() => {
    cy.doAdministratorLogin()
  })

  it('list view shows items', function () {
    cy.visit('administrator/index.php?option=com_foos')

    cy.get('main').should('contain.text', 'Astrid')
    cy.get('main').should('contain.text', 'Nina')
    cy.get('main').should('contain.text', 'Elmar')
    cy.get('main').should('contain.text', 'Sami')

    cy.checkForPhpNoticesOrWarnings()
  })
})

正如预期的那样,测试失败了。有红色的消息。您可以在左侧侧边栏中看到每个测试步骤的代码。因此,您可以找到错误的原因。对于每个步骤,都有一个HTML文档的快照,因此您可以在任何时候检查标记。这特别有助于开发期间。

The view after the test has failed.

在文件中只运行一个测试

我们的演示扩展包含多个布局。添加一个测试以测试空状态布局。由于我们现在在这个文件中有两个测试,Cypress将在每次保存文件时始终运行两个测试。我们可以使用.only()来确保只执行一个测试

// tests/System/integration/administrator/components/com_foos/FoosList.cy.js

describe('Test com_foos features', () => {
    beforeEach(() => {
        cy.doAdministratorLogin()
    })

    it('list view shows items', function () {
        cy.visit('administrator/index.php?option=com_foos')

        cy.get('main').should('contain.text', 'Astrid')
        cy.get('main').should('contain.text', 'Nina')
        cy.get('main').should('contain.text', 'Elmar')

        cy.checkForPhpNoticesOrWarnings()
    })

    it.only('emptystate layout', function () {
        cy.visit('administrator/index.php?option=com_foos&view=foos&layout=emptystate')
        cy.get('main').should('contain.text', 'No Foo have been created yet.')
    })
})

在开发过程中,这非常方便。

特殊测试属性

现在我们想测试我们的组件的前端。我们将在一个单独的文件/tests/System/integration/site/components/com_foos/FooItem.cy.js中这样做。

大多数情况下,我们在Joomla测试中使用CSS类来获取元素。虽然这是完全有效且可行的,但实际上并不推荐这样做。为什么呢?当你使用CSS类或ID时,你将你的测试绑定到可能会随时间改变的东西。类和ID用于设计、布局,有时通过JavaScript进行控制,这些都很容易改变。如果有人更改了类名或ID,你的测试将不再有效。为了使你的测试更加健壮且更具前瞻性,Cypress建议为你的元素创建特殊的数据属性,专门用于测试目的。

我将使用data-test属性来处理元素。首先,我在生产代码中添加了属性data-test="foo-main"

// /components/com_foos/tmpl/foo/default.php
<?php
\defined('_JEXEC') or die;
?>
<div data-test="foo-main">
Hello Foos
</div>

然后,我通过搜索属性[data-test="foo-main"]来测试生产代码。

// tests/System/integration/site/components/com_foos/FooItem.cy.js
describe('Test com_foo frontend', () => {
  it('Show frondend via query in url', function () {
    cy.visit('index.php?option=com_foos&view=foo')

    cy.get('[data-test="foo-main"]').should('contain.text', 'Hello Foos')

    cy.checkForPhpNoticesOrWarnings()
  })
})
测试菜单项以及对事件、等待和最佳实践的思考

现在,我喜欢测试我们组件的菜单项创建。我在一个独立的文件/tests/System/integration/administrator/components/com_foos/MenuItem.cy.js中这样做。这段代码很复杂,展示了许多特殊功能。

首先,我定义了一个常量,在其中设置了菜单项的所有相关属性。这样做的好处是,在相关属性发生变化的情况下,我只需要在一个地方进行调整。

const testMenuItem = {
  'title': 'Test MenuItem',
  'menuitemtype_title': 'COM_FOOS',
  'menuitemtype_entry': 'COM_FOOS_FOO_VIEW_DEFAULT_TITLE'
}

接下来,你可以看到文件MenuItem.cy.js的全部代码。

// tests/System/integration/administrator/components/com_foos/MenuItem.cy.js

describe('Test menu item', () => {
  beforeEach(() => {
    cy.doAdministratorLogin(Cypress.env('username'), Cypress.env('password'))
  })

  it('creates a new menu item', function () {
    const testMenuItem = {
      'title': 'Test MenuItem',
      'menuitemtype_title': 'COM_FOOS',
      'menuitemtype_entry': 'COM_FOOS_FOO_VIEW_DEFAULT_TITLE'
    }

    cy.visit('administrator/index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit')
    cy.checkForPhpNoticesOrWarnings()
    cy.get('h1.page-title').should('contain', 'Menus: New Item')

    cy.get('#jform_title').clear().type(testMenuItem.title)

    cy.contains('Select').click()
    cy.get('.iframe').iframe('#collapse1-heading').contains(testMenuItem.menuitemtype_title).click()
    cy.get('.iframe').iframe('#collapse1-heading').contains(testMenuItem.menuitemtype_entry).click()

    cy.intercept('index.php?option=com_menus&view=items&menutype=mainmenu').as('item_list')
    cy.clickToolbarButton('Save & Close')
    cy.wait('@item_list')
    cy.get('#system-message-container').contains('Menu item saved.').should('exist')

    // Frontend
    cy.visit('index.php')
    cy.get('.sidebar-right').contains(testMenuItem.title).click()
    cy.get('[data-test="foo-main"]').should('contain.text', 'Hello Foos')
    cy.checkForPhpNoticesOrWarnings()

    // Trash
    cy.visit('administrator/index.php?option=com_menus&view=items&menutype=mainmenu')
    cy.searchForItem(testMenuItem.title)
    cy.checkAllResults()
    cy.clickToolbarButton('Action')
    cy.intercept('index.php?option=com_menus&view=items&menutype=mainmenu').as('item_trash')
    cy.clickToolbarButton('trash')
    cy.wait('@item_trash')
    cy.get('#system-message-container').contains('Menu item trashed.').should('exist')

    // Delete
    cy.visit('administrator/index.php?option=com_menus&view=items&menutype=mainmenu')
    cy.setFilter('published', 'Trashed')
    cy.searchForItem(testMenuItem.title)
    cy.checkAllResults()
    cy.on("window:confirm", (s) => {
      return true;
    });
    cy.intercept('index.php?option=com_menus&view=items&menutype=mainmenu').as('item_delete')
    cy.clickToolbarButton('empty trash');
    cy.wait('@item_delete')
    cy.get('#system-message-container').contains('Menu item deleted.').should('exist')
  })
})
  • 在这段代码中,你可以看到测试某个东西然后删除它以恢复初始状态的一个例子。这样,你可以多次重复测试。如果不恢复初始状态,第二次测试运行将失败,因为Joomla无法存储两个相似的元素。

注意:测试应该是

  • 可重复的。
  • 保持简单。具体来说,这意味着它应该测试一个有限的问题,并且为这个问题编写的代码不应该太复杂。
  • 与其他测试独立。
  • 你可以看到如何使用通过cy.intercept()[^docs.cypress.io/api/commands/intercept]定义的拦截路由作为别名,然后使用cy.wait()[^docs.cypress.io/api/commands/wait]等待作为别名定义的路由。

在为这类应用程序编写测试时,人们往往会使用随机值,如cy.wait(2000);cy.wait命令中。这种方法的缺点是,虽然它在开发过程中可能工作得很好。但是,并不能保证它总是有效。为什么呢?因为底层系统依赖于难以预测的事物。因此,始终定义你确切在等待什么会更好。

  • 代码还展示了如何等待一个警告并确认它。
cy.on("window:confirm", (s) => {
  return true;
});
  • 最后但同样重要的是,测试代码包含Cypress内置和Joomla典型函数,这些函数可以被扩展开发者重用。例如,cy.setFilter('published', 'Trashed')cy.clickToolbarButton('Save & Close')是其中可以找到针对特定测试的解决方案的函数,这些函数特别适用于Joomla开发者。
混合异步和同步代码

Cypress命令是异步的,也就是说,它们不返回值,而是生成值。当我们启动Cypress时,它不会立即执行命令,而是按顺序读取它们并将它们排队。如果你在测试中混合了异步和同步代码,你可能会得到意外的结果。如果你运行以下代码,你将得到一个错误,这与预期不符。你肯定也期望mainText = $main.text()改变mainText的值。但mainText === 'Initial'在结束时仍然是有效的。为什么呢?Cypress首先在开始时执行同步代码,然后在结束时执行。然后它才调用then()中的异步部分。这意味着变量mainText被初始化,然后立即检查它是否已更改——当然,这是不可能的。

let mainText = 'Initial';
cy.visit('administrator/index.php?option=com_foos&view=foos&layout=emptystate')
cy.get("main").then(
  ($main) => (mainText = $main.text())
);

if (mainText === 'Initial') {
  throw new Error(`Der Text hat sich nicht geändert. Er lautet: ${mainText}`);
}

如果观察以下代码在浏览器控制台中的执行过程,队列的处理过程就会变得非常清晰和直观。文本 'Cypress Test.' 出现在 main 元素内容之前,尽管代码行顺序不同。

cy.get('main').then(function(e){
  console.log(e.text())
})
console.log('Cypress Test.')
存根和间谍

一个 存根 是模拟测试所依赖的函数行为的方法。不是调用实际函数,而是替换该函数并返回一个预定义的对象。它通常用于单元测试,但也可用于端到端测试。

一个 间谍存根 类似,但并不完全相同。它不会改变函数的行为,而是保持原样。它捕捉一些关于函数如何被调用的信息。例如,检查函数是否用正确的参数被调用,或者统计函数被调用的次数。

以下示例展示了 间谍存根 的实际应用。通过 const stub = cy.stub() 我们创建 存根 元素,并在下一步中确定第一次调用返回 false,第二次调用返回 true。使用 cy.on('window:confirm', stub) 我们使 存根 用于 window:confirm'。在下一步中,我们使用 cy.spy(win, 'confirm').as('winConfirmSpy') 创建 间谍 元素,它观察 'window:confirm' 的调用。现在我们测试第一次调用时拒绝删除类别,第二次调用时确认删除。在这个过程中,存根 确保我们可以确切地预期将提供哪些返回值。'window:confirm' 被封装。@winConfirmSpy 帮助确保函数实际上被调用过 - 以及调用了多少次。

// tests/System/integration/administrator/components/com_foos/FoosList.cy.js
...
const stub = cy.stub()

stub.onFirstCall().returns(false)
stub.onSecondCall().returns(true)

cy.on('window:confirm', stub)

cy.window().then(win => {
  cy.spy(win, 'confirm').as('winConfirmSpy')
})

cy.intercept('index.php?option=com_categories&view=categories&extension=com_foos').as('cat_delete')
cy.clickToolbarButton('empty trash');

cy.get('@winConfirmSpy').should('be.calledOnce')
cy.get('main').should('contain.text', testFoo.category)


cy.clickToolbarButton('empty trash');
cy.wait('@cat_delete')

cy.get('@winConfirmSpy').should('be.calledTwice')

cy.get('#system-message-container').contains('Category deleted.').should('exist')
...

如果只需为 'window:confirm' 调用设置一个固定值,以下代码就可以完成这项工作。

cy.on("window:confirm", (s) => {
  return true;
});

结论

在这篇文章中,您看到了使用 Cypress 进行端到端测试的基本理论和实用特性。我使用 Joomla 安装来演示如何编写不同的测试来确保网站上的 Joomla 组件按预期工作。我还展示了如何在 cypress.json 文件中自定义 Cypress 测试运行器以及如何使用自定义的 Cypress 命令。这一切都是通过易于理解的示例完成的。

希望您喜欢通过使用 Joomla 作为示例的 Cypress 之旅,并且能够从中获得许多知识和灵感。

在 Joomla 社区杂志上发表的一些文章代表了作者对特定主题的个人观点或经验,可能并不符合 Joomla 项目官方立场。

1
我第一次升级到 Joomla 5
Joomla 赢得多项 CMS 奖项(以及一个服务器!)...
 

评论 1

已经注册?这里登录
Diane 在 2023年11月14日星期二 12:20
出色的教程

非常感谢。帮助我很多,使事情得以顺利进行。

0
非常感谢。帮助我很多,使事情得以顺利进行。

接受后,您将访问由 https://magazine.joomla.net.cn/ 外部第三方提供的服务的部分。