使用 Galen 框架进行布局测试的艺术
已发表: 2022-03-10在设计图形用户界面时,总是有一个悬而未决的问题:我们如何对其进行自动化测试? 我们如何确保网站布局保持响应并在各种分辨率的各种设备上正确显示? 再加上动态内容、国际化和本地化要求带来的复杂性,这成为一个真正的挑战。
在本文中,我将引导您了解一种有趣的新布局测试技术。 使用 Galen 框架,我将提供一个详细的教程,用于编写有意义的通用布局测试,可以在任何浏览器和任何设备上执行,同时用作设计文档中的单一事实来源。
关于 SmashingMag 的进一步阅读:
- 用于响应式界面设计的视觉测试驱动开发
- 应用程序、游戏和移动网络测试自动化的基础知识
- 用于 React Native 应用程序的各种测试自动化框架
我还将展示我是如何为我们的分类网站 Marktplaats 上的消息传递页面提出优化测试的。 我们将学习如何用我们自己的语言扩展 Galen 的语法,如何改进测试代码以及如何将布局测试程序变成艺术。
盖伦框架简介
一年前的“响应式界面设计的可视化测试驱动开发”中介绍了 Galen 框架。 当时,它的语法是有限的。 自那以后,它有了很大的改进,并获得了许多新功能,我们将在这里进行介绍。
如果您不熟悉 Galen Framework,它是一个用于响应式和跨浏览器布局测试和功能测试的工具,具有自己的测试语言,名为 Galen Specs。 它基于 Selenium WebDriver,还具有丰富的 JavaScript API,可让您直接使用 WebDriver。 因为您可以控制 WebDriver,所以您可以在任何浏览器、云端(SauceLabs、BrowserStack、PerfectoMobile 等)或使用 Appium 的真实移动设备上运行测试。
安装和执行
设置 Galen 框架很容易。 只需执行以下命令即可通过 npm 安装它:
npm install -g galenframework-cli
如果您不使用 npm,只需下载最新的 Galen 框架存档,解压缩包并按照安装说明进行操作。
安装后,Galen Framework 可以通过多种方式启动。 例如,您可以使用check
命令启动单个页面的快速测试。 对于此命令,您需要提供一个带有布局验证的.gspec
文件,然后您可以像这样调用它:
galen check loginPage.gspec --url https://example.com --size 1024x768 --include desktop --htmlreport reports
此命令将启动浏览器,打开指定的 URL,将浏览器窗口大小调整为 1024 × 768 像素,并执行loginPage.gspec
文件中声明的所有验证。 结果,您将获得一份详细的 HTML 报告。
管理测试套件
在现实世界中,实际的 Web 应用程序并不纯粹由静态页面组成。 很多时候,您必须执行一些操作才能到达您要检查的地方。 在这种情况下,Galen 提供了 JavaScript 测试套件和 GalenPages JavaScript API 来实现页面对象模型。 下面是一个 JavaScript Galen 测试的简单示例:
test("Home page", function() { var driver = createDriver("https://galenframework.com", "1024x768"); checkLayout(driver, "homePage.gspec", ["desktop"]); });
这是一个登录页面的页面对象模型的实现,取自一个真实的项目。
WelcomePage = $page("Welcome page", { loginButton: "#welcome-page .button-login" }); LoginPage = $page("Login page", { username: "input[name='login.username']", password: "input[name='login.password']", loginButton: "button.button-login" loginAs: loggedFunction ("Log in as ${_1.username} with password ${_1.password}", function(user) { this.username.typeText(user.username); this.password.typeText(user.password); this.loginButton.click(); }) }); test("Login page", function() { var driver = createDriver("https://testapp.galenframework.com", "1024x768"); var welcomePage = new WelcomePage(driver).waitForIt(); welcomePage.loginButton.click(); new LoginPage(driver).waitForIt(); checkLayout(driver, "loginPage.gspec", ["desktop"]); });
对于高级用法,我建议您查看 Galen Bootstrap 项目。 它是专门为 Galen 构建的 JavaScript 扩展。 它为 UI 测试提供了一些额外的功能,并为配置浏览器和执行复杂的测试套件提供了一种更简单的方法。
简单布局测试
让我首先在 Galen 框架中介绍一个简单的布局测试。 然后,我将继续讨论高级用例并演示如何扩展 Galen Specs 语法。 为此,我们将查看带有图标和标题的标题:

在 HTML 代码中,它可能看起来像这样:
<body> <!-- … --> <div> <img class="header-logo" src="/imgs/header-logo.png"/> <h1>My Blog</h1> </div> <!-- … --> </body>
Galen 布局测试的最简单形式如下所示。 首先,我们必须使用 CSS 选择器声明对象。
@objects header #header icon #header img caption #header h1
然后,我们用有意义的名称声明一个测试部分,并将我们所有的验证放在它下面。
= Icon and Caption = icon: left-of caption 10 to 15px width 32px height 32px inside header 10px top caption: aligned horizontally all header inside header
在这里,我们测试了两个标题元素:图标和标题。 图标和标题元素下列出的所有验证实际上都是标准的 Galen Specs。 这些规范是您可以组装自己的布局测试解决方案的基本构建块。 每个规范都会验证单个属性(例如宽度、高度、文本)、相对定位(例如内部、左侧、上方)或屏幕截图中的像素(例如配色方案、图像)。
使用 forEach 循环测试多个元素
前面的示例演示了一个简单的场景。 让我们看看如何处理更复杂的情况:水平菜单。 首先,让我们尝试一种简单的布局测试技术。

首先匹配页面上的多个元素。 通过以下代码,我们告诉 Galen 搜索与#menu ul li
CSS 选择器匹配的元素。
@objects menu #menu item-* ul li
稍后,我们可以使用menu.item-1
和menu.item-2
等名称引用这些项目,并使用@forEach
循环遍历所有菜单项。
= Menu = menu.item-1: inside menu 0px top left bottom @forEach [menu.item-*] as menuItem, next as nextItem ${menuItem}: left-of ${nextItem} 0px aligned horizontally all ${nextItem}
如您所见,即使实际检查没有那么复杂,代码已经变得不那么直观了。 想象一下,如果我们的测试中有更多类似的代码。 在某些时候,它会变成一个无法维护的烂摊子。 应该有办法改进它。
重新思考布局测试
如果您考虑前面的示例,似乎我们可以用一两句话来表达布局。 例如,我们可以这样说,“所有菜单项都应该水平对齐,中间没有边距。 第一个菜单项应该位于菜单的左侧,没有边距。” 因为我们已经制定了句子来解释我们想要的布局,为什么我们不能在我们的代码中使用它们呢? 想象一下,如果我们可以这样写代码:
= Menu = |first menu.item-* is in top left corner of menu |menu.item-* are aligned horizontally next to each other
事实上,这是从我的项目中复制而来的真实工作代码。 在最后两行(从管道|
开始)中,我们调用了自定义函数,这些函数通过解析这两个语句来收集它们的参数。 当然,上面的示例不会按原样工作。 为了让它编译,我们需要为这两个语句实现处理程序。 我们稍后会回到这个实现。
上面例子的关键点是布局测试已经从对象驱动转向表达式驱动测试。 从这样一个小例子中可能并不明显,但在更大的范围内肯定是显而易见的。 那么,为什么这很重要? 简短的回答是,这会改变我们的思维方式并影响我们设计软件和为其编写测试的方式。
使用这种技术,我们不会将我们的页面视为一堆具有特定关系的对象。 我们不测试单个元素的 CSS 属性。 我们避免编写复杂的非平凡代码。 相反,我们试图考虑常见的布局模式和有意义的陈述。 我们不是单独测试菜单项 1、菜单项 2 等,而是应用以下通用语句:
- 可在其他元素上重现;
- 不包含硬编码的像素值;
- 应用于抽象而非具体对象;
- 最后但并非最不重要的一点是,当我们阅读它们时实际上是有意义的。
这个怎么运作
让我用这个简单的例子来解释自定义布局表达式的机制:

在这个例子中,我们应该检查按钮是否拉伸到面板,左侧或右侧没有边距。 如果没有自定义规则,我们可以通过不同的方式来处理它,但我更喜欢以下解决方案:
button: inside some_panel 0px left right
上面的代码让我们可以灵活地在边上声明自定义边距,并且它还隐式测试按钮是否完全包含在面板中。 缺点是它的可读性不是很好,这就是为什么我要把这个验证放在表达式button stretches to some_panel
。 为了让它起作用,我们需要编写一个这样的自定义规则:
@rule %{elementName} stretches to %{parentName} ${elementName}: inside ${parentName} 0px left right
而已。 现在我们只需一行就可以将它放入我们的测试中:
| button stretches to some_panel
如您所见,此规则采用两个参数: elementName
和parentName
。 这使我们也可以将其应用于其他元素。 只需替换这两个对象的名称即可。
| login_panel stretches to main_container | header stretches to screen | footer stretches to screen # etc.
实现你自己的测试语言
让我们回到水平菜单布局表达式的初始示例。
= Menu = | first menu.item-* is in top left corner of menu | menu.item-* are aligned horizontally next to each other
我们可以通过以下方式实现第一条规则:
@rule first %{itemPattern} is in %{cornerSides} corner of %{parentElement} @if ${count(itemPattern) > 0} ${first(itemPattern).name}: inside ${parentElement} 0px ${cornerSides}
在我们的示例中使用时,它将按如下方式解析参数:
-
itemPattern
=menu.item-*
-
cornerSides
=top left
-
parentElement
=menu
因为我们已经完成了第一个表达式,我们可以移动到下一个表达式。 在第二个表达式中,我们应该测试所有菜单项的水平对齐方式。 我建议三个简单的步骤:
- 查找所有菜单项。
- 遍历所有这些直到倒数第二个元素。
- 检查元素
n
是否位于元素n+1
的左侧,并且它们的顶部和底部边缘对齐。
为了让它工作,我们需要一个@forEach
循环和规范left-of
和aligned
。 幸运的是,在 Galen 中,您可以在循环中引用上一个或下一个项目。 如果您声明了对下一个元素的引用,它只会迭代到倒数第二个元素,这正是我们所需要的。
@rule %{itemPattern} are aligned horizontally next to each other @forEach [${itemPattern}] as item, next as nextItem ${item}: left-of ${nextItem} 0px aligned horizontally all ${nextItem}
您可能会问,如果我们必须在测试中指定边距(例如~ 20px
或10 to 20px
)怎么办? 然后,我建议实施单独的规则或扩展现有规则以支持%{margin}
参数。
@rule %{itemPattern} are aligned horizontally next to each other with %{margin} margin @forEach [${itemPattern}] as item, next as nextItem ${item}: left-of ${nextItem} ${margin} aligned horizontally all ${nextItem}
而已! 我们创建了一个通用表达式来帮助我们验证水平菜单。 然而,由于它的灵活性,我们可以做的远不止这些。 我们可以使用它来测试页面上的任何其他元素。 我们甚至可以用它来测试两个按钮的对齐情况:

| menu.item-* are aligned horizontally next to each other with 0px margin | submit_button, cancel_button are aligned horizontally next to each other with 20px margin
您可能会注意到,在这两个示例中,我们以两种不同的方式声明了第一个参数。 在第一个表达式中,第一个参数是“menu.item-*”
,而在第二个表达式中,它被声明为“submit_button, cancel_button”
。 这是可能的,因为@forEach
循环允许我们使用逗号分隔的对象列表和星号运算符。 但是我们还没有完成重构。 我们可以进一步改进代码并使其更具可读性。 如果我们为菜单项和登录表单按钮创建组,我们可以完成这样的事情:
@groups menu_items menu_item-* login_form_buttons submit_button, cancel_button = Testing login page = | &menu_items are aligned horizontally next to each other with 0px margin | &login_form_buttons are aligned horizontally next to each other with 20px margin
在这种情况下,我们必须使用&
符号,它代表组声明。 这已经是一个很好的测试了。 首先,它可以正常工作,我们能够测试我们需要的东西。 此外,代码清晰易读。 如果另一个人要问你登录页面应该是什么样子,设计要求是什么,你可以告诉他们看测试。

如您所见,为复杂的布局模式实现自定义表达式并不是什么大问题。 一开始可能很有挑战性,但它仍然类似于某种创造性活动。
动态边距
让我们看看您有时会在各种网站上找到的另一种罕见的布局模式。 如果我们想测试元素之间的距离相等怎么办? 让我们尝试为此实现另一个规则,但这次使用 JavaScript 实现。 我提出这样的说法: “box_item-* are aligned horizontally next to each other with equal distance”
。 这会有点棘手,因为我们不知道元素之间的边距,我们不能只硬编码像素值。 因此,我们要做的第一件事就是检索第一个和最后一个元素之间的实际边距。

一旦我们获得该边距,我们就可以在@forEach
循环中声明它,类似于我们之前所做的。 我建议使用 JavaScript API 来实现此规则,因为所需的逻辑比我们之前的所有示例都复杂一些。 让我们创建一个名为my-rules.js
的文件并输入以下代码:
rule("%{objectPattern} are aligned horizontally next to each other with equal margin", function (objectName, parameters) { var allItems = findAll(parameters.objectPattern), distance = Math.round(Math.abs(allItems[1].left() - allItems[0].right())), expectedMargin = (distance - 1) + " to " + (distance + 1) + "px"; if (allItems.length > 0) { for (var i = 0; i < allItems.length - 1; i += 1) { var nextElementName = allItems[i + 1].name; this.addObjectSpecs(allItems[i].name, [ "aligned horizontally all " + nextElementName, "left-of " + nextElementName + " " + expectedMargin ]); } } });
在我们的测试代码中,我们将这样使用它:
@script my-rules.js # … = Boxes = | box_item-* are aligned horizontally next to each other with equal distance
如您所见,在 Galen Framework 中,我们在实现规则时可以在两种语言之间进行选择:Galen Specs 和 JavaScript。 对于简单的表达式,Galen Specs 更容易使用,但对于复杂的表达式,我总是选择 JavaScript。 如果您想了解有关 JavaScript 规则的更多信息,请参阅文档。
盖伦特辑
玩够了各种布局模式后,我意识到所有这些 Galen 规则都可以轻松应用于任何其他测试项目。 这给了我一个想法,将最常见的布局表达式编译到他们自己的库中。 这就是我创建 Galen Extras 项目的原因。 以下是该库功能的一些示例:
| header.icon should be squared | amount of &menu_items should be > 3 | &menu_items are aligned horizontally next to each other | &list_items are aligned vertically above each other with equal distance | every &menu_item is inside menu 0px top and has width > 50px | first &menu_item is inside menu 0px top left | &menu_items are rendered in 2 column table | &menu_items are rendered in 2 column table, with 0 to 1px vertical and 10px horizontal margin | &login_form_elements sides are vertically inside content_container with 20px margin login_panel: | located on the left side of panel and takes 70 % of its width # etc …
Galen Extras 库包含许多您在网站上常见的布局模式,我会在发现有用的模式后立即对其进行更新。 一旦建立了这个库,我决定在一个真正的测试项目中尝试它。
测试消息应用程序
目前,我在 Marktplaats 担任软件工程师。 在某个时候,我决定将我获得的所有经验应用到一个真实的项目中。 我需要测试我们网站上的消息传递页面。 这是它的样子:

老实说,对这些页面实施测试对我来说总是有点吓人,尤其是布局测试。 但是有了 Galen Extras 库,它实际上运行得非常顺利,很快我就可以想出这段代码:
@import ../selected-conversation.gspec @groups (message, messages) messenger.message-* first_two_messages messenger.message-1,messenger.message-2 first_message messenger.message-1 second_message messenger.message-2 third_message messenger.message-3 (message_date_label, message_date_labels) messenger.date_label-* first_date_label messenger.date_label-1 second_date_label messenger.date_label-2 = Messages panel = = Messages and Date labels = |amount of visible &message_date_labels should be 1 |first &message_date_label has text is "17 november 2015" |amount of visible &messages should be 3 |&first_two_messages should be located at the left inside messenger with ~ 20px margin |&third_message should be located at the right inside messenger with ~ 20px margin |&messages are placed above each other with 10 to 15px margin |text of all &messages should be ["Hi there!", "I want to buy something", "Hello! Sure, it's gonna be 100 euros"] = Styling = |&first_two_messages should be styled as others message |&third_message should be styled as own message
提取像素范围
测试看起来不错:它紧凑且可读,但仍远非完美。 我真的不喜欢所有这些边距定义( ~ 20px
, 10 to 15px
)。 其中一些是重复的,很难理解它们各自代表什么。 这就是为什么我决定将每个边距隐藏在一个有意义的变量后面。
# ... @set messages_side_margin ~ 20px messages_vertical_margin 10 to 15px = Messages panel = = Messages and Date labels = |amount of visible &message_date_labels should be 1 |first &message_date_label has text is "17 november 2015" |amount of visible &messages should be 3 |&first_two_messages should be located at the left inside messenger with ${messages_side_margin} margin |&third_message should be located at the right inside messenger with ${messages_side_margin} margin |&messages are placed above each other with ${messages_vertical_margin} margin # ...
如您所见,我已将边距移至messages_vertical_margin
和messages_side_margin
。 我还声明了一个minimal
边距,范围在 0 到 1 像素之间。
# ... @set minimal 0 to 1px = Conversations Panel = | &conversations are aligned above each other with ${minimal} margin # ...
基于图像的验证和自定义表达式
在介绍了页面上所有主要元素的位置之后,我决定还测试样式。 我想验证每条消息是否具有特定于用户角色的背景颜色。 当用户登录时,消息将具有浅蓝色背景。 其他用户的消息将具有白色背景。 如果未发送消息,则错误警报将具有粉红色背景。 以下是帮助我验证这些样式的规则:
@set OWN_MESSAGE_COLOR #E1E8F5 OTHERS_MESSAGE_COLOR white ERROR_MESSAGE_COLOR #FFE6E6 @rule %{item} should be styled as %{style} message ${item}: @if ${style === "own"} color-scheme > 60% ${OWN_MESSAGE_COLOR}, 0.2 to 20 % ${MAJOR_TEXT_MESSAGE_COLOR} @elseif ${style === "error"} color-scheme > 60% ${ERROR_MESSAGE_COLOR}, 0.2 to 20 % ${MAJOR_TEXT_MESSAGE_COLOR} @else color-scheme > 60% ${OTHERS_MESSAGE_COLOR}, 0.2 to 20 % ${MAJOR_TEXT_MESSAGE_COLOR}
color-scheme
规范验证元素内颜色的比例分布。 它裁剪页面的屏幕截图并分析颜色分布。 因此,要检查元素的背景颜色,我们只需检查其分布是否大于总颜色范围的 60%。 在测试中,这条规则的调用如下:
= Styling = |&first_two_messages should be styled as others message |&third_message should be styled as own message
配置测试套件
消息传递应用程序是与 RESTful 消息传递 API 一起使用的动态应用程序。 因此,要测试它在所有不同状态下的布局,我们必须准备测试数据。 我决定模拟 Messaging API,以便能够在测试套件中配置我的所有测试消息。 这是我的测试套件的一个片段,它显示了我们的测试是如何构建的。
// ... testOnAllDevices("Unselected 2 conversations", "/", function (driver, device) { mock.onGetMyConversationsReturn(sampleConversations); refresh(driver); new MessageAppPage(driver).waitForIt(); checkLayout(driver, "specs/tests/unselected-conversations.gspec", device.tags); }); testOnAllDevices("When clicking a conversation it should reveal messages", "/", function (driver, device) { mock.onGetMyConversationsReturn(sampleConversations); mock.onGetSingleConversationReturn(sampleMessages); refresh(driver); var page = new MessageAppPage(driver).waitForIt(); page.clickFirstConversation(); checkLayout({ driver: driver, spec: "specs/tests/three-simple-messages-test.gspec", tags: device.tags, vars: { expectedTextProvider: textProvider({ "messenger.message-1": "Hi there!\n11:02", "messenger.message-2": "I want to buy something\n12:02", "messenger.message-3": "Hello! Sure, it's gonna be 100 euros\n13:02" }) } }); }); // ...
捕捉错误
实现这些简单的表达式很快就会得到回报。 让我们来看看我们可以用我们的测试套件捕获什么样的错误。
造型问题
这是一个示例,说明当我们的 CSS 代码库出现问题时,所有消息都以相同的背景呈现。

如果将此屏幕截图与原始屏幕截图进行比较,您会注意到最后一条消息具有白色背景,而它应该是浅蓝色的。 让我们看看 Galen 是如何报告这个问题的:

对于突出显示的对象,它color #e1e8f5 on “messenger.message-3” is 0% but it should be greater than 60%
。 老实说,这个错误信息看起来不是很清楚,但是因为这个检查是根据自定义规则生成的,所以我们总是可以在报告分支中查找它的原始名称:

如果向上滚动,您会看到原始语句&third_message should be styled as own message
。 这是使用自定义表达式的另一个好处:它们可以帮助您理解失败并很好地描述所有这些生成的验证。
定位问题
这是另一个由于元素对齐不正确而导致布局出错的示例。 在下面的屏幕截图中,您可以看到最后一条消息位于消息视口的左侧,而不是右侧。

让我们再看一下带有错误消息的屏幕截图:

屏幕截图突出显示了消息传递容器和最后一个消息元素。 与此同时,它显示以下错误消息: “messenger.message-3” is 285px right which is not in range of 22 to 28px
。 Web 开发人员可能不清楚为什么右侧需要 22 到 28 像素的边距。 同样,您必须在报告分支中查找验证语句:

该检查的原始语句是&third_message should be located at the right inside messenger with ~ 25px margin
。 这更有意义。 而且,其他前端工程师即使没有写过测试,也会看懂这份测试报告。
布局测试指南
考虑到所有这些不同的实验,我决定将所有的学习形式化为通用布局测试指南。 以下是简化测试程序的步骤的快速清单。
- 识别设计中的布局模式。
- 概括验证语句。 尝试将大多数验证压缩为单个句子。
- 组件化! 将重复元素的测试转移到专用组件中总是更好。
- 为部分、规则和对象使用有意义的名称。
- 避免像素。 尝试用有意义的变量替换像素值(无论是精确值还是范围)。
- 调整您网站的代码,使其更易于测试。 这将帮助您构建和维护您的生产和测试代码。
救援验收标准
我经常会收到这样的问题,“那么布局测试应该有多详细? 我们应该具体测试什么?” 很难给出一个普遍的答案。 问题是当测试的覆盖率很小时,你会错过错误。 另一方面,如果您的测试过于详细,您可能会得到很多误报,并且将来您可能会迷失在测试维护中。 所以,有一个权衡。 但我确实为自己找到了一个通用的指导方针。 如果您将工作拆分为更小的用户故事,那么以验收标准的形式构建页面设计会变得更容易。 最后,您可能会将这个验收标准正确地放入您的测试代码中。 例如,其中一些语句可以以自定义规则的形式定义,如前面所有代码示例所示。 验收标准的一个很好的例子是这样的:
- 弹出窗口应在屏幕上垂直和水平居中。
- 它们应该是 400 像素宽。
- 按钮应水平对齐。
- 等等
一旦你用简单的句子描述了设计,就可以更容易地将它们转换为可重用的语句并组织你的测试代码。
结论
如您所见,这样的练习可以帮助您构建设计并发现可以跨多个页面组件共享的一般布局模式。 即使页面很复杂并且包含很多元素,您也总能找到一种方法在布局表达式中对它们进行分组。 使用这种方法,布局测试更多地成为测试驱动开发的工具,帮助您逐步设计、实施和交付软件,这在敏捷环境中特别有用。
资源
- 盖伦框架(官网)
- 盖伦框架,GitHub
- Galen Extras(库),GitHub
- 盖伦引导,GitHub
- “盖伦框架教程”,YouTube