本文章有视频讲解录屏,欢迎查看:Engineering-knowledge-share-e2e-test - Google 云端硬盘
Gitlab 文档给的定义是:
End-to-end (e2e) testing is a strategy used to check whether your application works as expected across the entire software stack and architecture
E2E 测试的特征是黑盒、(大多数情况下)测试时调用浏览器。在 Gitlab 项目中,我们很容易区分 E2E 测试与其它测试 —— 前者都放置在 qa/qa
目录中。
是的,Gitlab 不推荐写(过于详细的) E2E 测试。理由是
Gitlab 中将测试分为四类:单元测试、集成测试、系统级白盒测试和 E2E 测试。
(图中没有画出系统级白盒测试,但你肯定知道它应该画在哪)
在现存在测试用例分布中,E2E 测试的占比是非常少的:
测试等级 | 用例个数的占比 |
---|---|
E2E 测试 | 0.17% |
系统白盒测试 | 10.9% |
集成测试 | 17.9% |
单元测试 | 71.0% |
(数据来源:Testing levels|GitLab)
不久前我完成了 Webhook 新功能的开发,将原来的简单输入框,扩展为支持三种模式下输入框,就像这样:
Before | After |
---|---|
![]() |
![]() |
考虑到原来已经存在 Webhook 的 E2E 测试,并且我也完成了详细的单元测试和集成测试,那么我就不需要再写 E2E 测试了,甚至不需要更新已有的 E2E 测试。
我会先给出一个 Hello world 的例子,然后介绍 E2E 测试独有的三个重要知识点。
要了解更详细的编写规范,请查看官方文档:End-to-end Testing。
Gitlab E2E 测试和单元测试一样使用 Ruby RSpec 语法。(如果你对 RSpec 不熟悉,可以来这里了解一下:RSpec)
module QA
RSpec.describe 'Manage' do
describe 'Login' do
before do
Flow::Login.sign_in
end
it 'can login' do
Page::Main::Menu.perform do |menu|
expect(menu).to be_signed_in
end
end
end
end
end
如果你看得仔细,会发现熟悉的结构中其实藏着一些陌生的东西。比如
Flow::Login
是什么RSpec.describe 'Manage' do
中的 Manage
有什么特殊含义吗?Page::Main::Menu
是什么,为什么 expect
语句要在 Page
的块里执行不用怕,Flow
只是我们定义一些流程函数的类,它过于简单,本文不再赘述,官方文档值得一看:Flows in GitLab QA|GitLab。
至于 RSpec.describe 'Manage' do
和 Page
,下面来解释。
在其他测试中,RSpec.describe
后的标题要可以是类、模块甚至一段没有规范的描述性字符串。但在 E2E 测试的标题没那么自由,它应当是 DevOps 阶段之一。
就像上述例子中的 Manage
,它表示测试的“登录”功能属于 DevOps 的第一阶段 Manage。
你要仔细阅读相关文档,根据你要测试的内容,选择一个合适的阶段,这是完成 E2E 测试的第一步。
在已经存在 E2E 测试中,经常会出现这类代码
Page::MergeRequest::New.perform do |merge_request|
...
end
其中 Page::MergeRequest
叫做 MergeRequest 页面的 Page Object。
关于 Page Object 的详细文档,可以查看:Page objects in GitLab QA。
Page Object 的作用是,完成页面 DOM 结构与测试用例的解耦。
“解耦”这件事太重要了,假如你不这么认为,想象一下你写过好多个测试用例,它们在测试代码中直接操作页面元素(各种输入框和按钮),然后你就会遇到棘手的问题:
我们在前端代码中常看到的 data-qa-selector
属性,就是为 Page Object 服务的。Page Object 实现了各类方法,它们通过定位到 DOM 元素,实现业务上的各类功能,提供给测试用例使用。
一个典型的例子是登录页面
module QA
module Page
module Main
class Login < Page::Base
view 'path/of/login.haml' do
element :username_field
element :password_field
element :sign_in_button
end
def sign_in(user)
fill_element :username_field, user.ldap_username
fill_element :password_field, user.ldap_password
click_element :sign_in_button
end
end
end
end
end
代码很简洁,只由两部分组成:view
块和一些函数。
虽然 view
是一个新的用法,但你完全不用怕它,因为这里几乎只有一个关键字:element
,用来对应前端代码中“埋”好的 data-qa-selector
属性。
小结一下如何写一个 Page 对象:
data-qa-selector
定义元素view
块中sign_in
在已经存在的 E2E 测试中,经常(甚至比 Page 对象更常见)会出现这类代码
let(:project) do
Resource::Project.fabricate! do |project|
project.name = "project1"
end
end
这就是 Project 的 Resource 类 Resource::Project
在使用自己的方法 fabricate!
来创建资源。和 Rails 中的 resource
概念类似,我们也应该为测试中出现的资源抽象出一个 Resource 主体。
Resource 的主要功能是创建资源,所以按照规范,必须要实现 fabricate!
方法(当然了,这是你写 Resource 的目的)。其他可选的方法是 api_get_path
、api_post_path
和 api_post_body
等,分别用来获取已存在的资源和提供创建接口需要的参数。
例如
module QA
module Resource
class Project < Base
attr_accessor :name
def fabricate!
Page::Project.perform do |show|
show.go_to_new_project
end
Page::Project::New.perform do |new_project|
new_project.set_name(name)
new_project.create_project!
end
end
end
end
end
单元测试同样没有使用 Feature.enable
,而是使用新实现的 stub_feature_flags
。这两个方法内部实现几乎没有区别,只是后者增加了一些日志,并且支持同时操作多个 feature flag。
但 E2E 测试就很不一样了,它并没有在 Rails 运行时上下文中,无法直接调用 Feature.enable
。E2E 实现了 Runtime::Feature.enable
方法,通过 Http 调用 Rails 的 Resuful 接口来实现相同的功能。
let
关键字在 E2E 测试中不那么受欢迎一些“昂贵”的操作(例如创建 Project Resource),且可以在多个测试用例中共享此资源时,我们推荐使用实例变量而不是 let
。
通常情况下,Gitlab 推荐使用 let
而不是实例变量,这通常是看上了 let
的惰性加载这个好处 —— 用几次就调用几次。但在 E2E 中,一些创建操作是非常耗时的,它们通常使用接口甚至 UI 来创建,这使得 let
多次加载的特性显得尤为糟糕。