测试级别
此图展示了我们使用的每种测试类型的相对优先级。e2e 代表端到端(end-to-end)。
截至 2025-02-03,我们按级别估算的测试分布如下:
| 测试级别 | 社区版 (Community Edition) | 企业版 (Enterprise Edition) | 社区版+企业版 (Community + Enterprise Edition) |
|---|---|---|---|
| 系统级黑盒测试(也称为端到端测试或 QA 测试) | 401 (0.14%) | 303 (0.10%) | 704 (0.24%) |
| 系统级白盒测试(也称为系统测试或功能测试) | 8,362 (2.90%) | 4,082 (1.41%) | 12,444 (4.31%) |
| 集成测试 | 39,716 (13.76%) | 17,411 (6.03%) | 57,127 (19.79%) |
| 单元测试 | 139,504 (48.32%) | 78,955 (27.35%) | 218,459 (75.66%) |
单元测试
正式定义:https://en.wikipedia.org/wiki/Unit_testing
这类测试确保单个代码单元(一个方法)能按预期工作(给定输入,产生可预测的输出)。这些测试应尽可能隔离。例如,不与数据库交互的模型方法不需要数据库记录。不需要数据库记录的类应尽可能使用桩(stubs)/替身(doubles)。
| 代码路径 | 测试路径 | 测试引擎 | 说明 |
|---|---|---|---|
app/assets/javascripts/ |
spec/frontend/ |
Jest | 更多详情请参见 前端测试指南 部分。 |
app/finders/ |
spec/finders/ |
RSpec | |
app/graphql/ |
spec/graphql/ |
RSpec | |
app/helpers/ |
spec/helpers/ |
RSpec | |
app/models/ |
spec/models/ |
RSpec | |
app/policies/ |
spec/policies/ |
RSpec | |
app/presenters/ |
spec/presenters/ |
RSpec | |
app/serializers/ |
spec/serializers/ |
RSpec | |
app/services/ |
spec/services/ |
RSpec | |
app/uploaders/ |
spec/uploaders/ |
RSpec | |
app/validators/ |
spec/validators/ |
RSpec | |
app/views/ |
spec/views/ |
RSpec | |
app/workers/ |
spec/workers/ |
RSpec | |
bin/ |
spec/bin/ |
RSpec | |
config/ |
spec/config/ |
RSpec | |
config/initializers/ |
spec/initializers/ |
RSpec | |
config/routes.rb, config/routes/ |
spec/routing/ |
RSpec | |
config/puma.example.development.rb |
spec/rack_servers/ |
RSpec | |
db/ |
spec/db/ |
RSpec | |
db/{post_,}migrate/ |
spec/migrations/ |
RSpec | 更多详情请参见 测试 Rails 迁移指南。 |
Gemfile |
spec/dependencies/, spec/sidekiq/ |
RSpec | |
lib/ |
spec/lib/ |
RSpec | |
lib/tasks/ |
spec/tasks/ |
RSpec | |
rubocop/ |
spec/rubocop/ |
RSpec | |
spec/support/ |
spec/support_specs/ |
RSpec |
前端单元测试
单元测试处于最低的抽象级别,通常测试用户无法直接感知的功能。
graph RL
plain[普通 JavaScript];
Vue[Vue 组件];
feature-flags[功能开关];
license-checks[许可证检查];
plain---Vuex;
plain---GraphQL;
Vue---plain;
Vue---Vuex;
Vue---GraphQL;
browser---plain;
browser---Vue;
plain---backend;
Vuex---backend;
GraphQL---backend;
Vue---backend;
backend---database;
backend---feature-flags;
backend---license-checks;
class plain tested;
class Vuex tested;
classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
classDef label stroke-width:0;
classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
subgraph " "
tested;
mocked;
class tested tested;
end
何时使用单元测试
- 导出的函数和类: 任何导出的内容都可能在你无法控制的地方被重用。 你应该通过测试来记录公共接口的预期行为。
- Vuex actions: 任何 Vuex action 都必须以一致的方式工作,独立于触发它的组件。
- Vuex mutations: 对于复杂的 Vuex mutations,你应该将测试与其他部分分离,以便简化问题排查。
何时不使用单元测试
- 未导出的函数或类: 未从模块导出的内容可视为私有或实现细节,无需测试。
- 常量: 测试常量的值意味着复制它,这会带来额外的工作量,而不能增加对值正确性的额外信心。
- Vue 组件: 计算属性、方法和生命周期钩子可视为组件的实现细节,已被组件测试隐式覆盖,无需单独测试。 更多信息,请参见 官方 Vue 指南。
单元测试中应该模拟什么
- 被测类的状态: 直接修改被测类的状态,而不是使用类的方法,可以避免测试设置中的副作用。
- 其他导出的类: 每个类都必须独立测试,以防止测试场景呈指数级增长。
- 作为参数传递的单个 DOM 元素: 对于只操作单个 DOM 元素而非整个页面的测试,创建这些元素比加载整个 HTML fixture 更高效。
- 所有服务器请求: 运行前端单元测试时,后端可能无法访问,因此所有出站请求都需要被模拟。
- 异步后台操作: 后台操作无法停止或等待,因此它们会在后续测试中继续运行并产生副作用。
单元测试中不应该模拟什么
- 未导出的函数或类: 所有未导出的内容可视为模块私有内容,并通过导出的类和函数隐式测试。
- 被测类的方法: 通过模拟被测类的方法,测试的是模拟对象而非真实方法。
- **工具函数(纯函数,或仅修改参数的函数): 如果函数没有副作用(因为它没有状态),在测试中不模拟它是安全的。
- 完整的 HTML 页面: 避免在单元测试中加载完整页面的 HTML,因为这会减慢测试速度。
前端组件测试
组件测试覆盖单个组件的状态,该状态会根据外部信号(如用户输入、其他组件触发的事件或应用状态)而变化,且这些状态对用户是可见的。
graph RL
plain[普通 JavaScript];
Vue[Vue 组件];
feature-flags[功能开关];
license-checks[许可证检查];
plain---Vuex;
plain---GraphQL;
Vue---plain;
Vue---Vuex;
Vue---GraphQL;
browser---plain;
browser---Vue;
plain---backend;
Vuex---backend;
GraphQL---backend;
Vue---backend;
backend---database;
backend---feature-flags;
backend---license-checks;
class Vue tested;
classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
classDef label stroke-width:0;
classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
subgraph " "
tested;
mocked;
class tested tested;
end
何时使用组件测试
- Vue 组件
何时不使用组件测试
- Vue 应用: Vue 应用可能包含许多组件。 在组件级别测试它们需要太多精力。 因此它们在前端集成级别进行测试。
- HAML 模板: HAML 模板只包含标记,没有前端逻辑。 因此它们不是完整的组件。
组件测试中应该模拟什么
- 副作用: 任何可能改变外部状态的内容(例如网络请求)都应该被模拟。
- 子组件:
每个组件都单独测试,因此子组件被模拟。
另请参见
shallowMount()
组件测试中不应该模拟什么
- 被测组件的方法或计算属性: 通过模拟被测组件的一部分,测试的是模拟对象而非真实组件。
- Vuex: 保持 Vuex 不被模拟,以避免脆弱和误报的测试。 使用 mutations 将 Vuex 设置为适当的状态。 模拟副作用,而不是 Vuex actions。
集成测试
正式定义:https://en.wikipedia.org/wiki/Integration_testing
这类测试确保应用程序的各个部分能良好协作,无需实际应用环境(如浏览器)的开销。 这些测试应在请求/响应级别断言:状态码、头部、正文。 例如,它们对于测试权限、重定向、API 端点、渲染的视图等很有用。
| 代码路径 | 测试路径 | 测试引擎 | 说明 |
|---|---|---|---|
app/controllers/ |
spec/requests/, spec/controllers |
RSpec | 请求规范(request specs)优先于传统的控制器规范(controller specs)。API 端点推荐使用请求规范。 |
app/mailers/ |
spec/mailers/ |
RSpec | |
lib/api/ |
spec/requests/api/ |
RSpec | |
app/assets/javascripts/ |
spec/frontend/ |
Jest | 更多详情见下文 |
前端集成测试
集成测试覆盖单个页面上所有组件之间的交互。 它们的抽象级别与用户与 UI 交互的方式相当。
graph RL
plain[普通 JavaScript];
Vue[Vue 组件];
feature-flags[功能开关];
license-checks[许可证检查];
plain---Vuex;
plain---GraphQL;
Vue---plain;
Vue---Vuex;
Vue---GraphQL;
browser---plain;
browser---Vue;
plain---backend;
Vuex---backend;
GraphQL---backend;
Vue---backend;
backend---database;
backend---feature-flags;
backend---license-checks;
class plain tested;
class Vue tested;
class Vuex tested;
class GraphQL tested;
class browser tested;
linkStyle 0,1,2,3,4,5,6 stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
classDef label stroke-width:0;
classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
subgraph " "
tested;
mocked;
class tested tested;
end
何时使用集成测试
- 页面包(
app/assets/javascripts/pages/中的index.js文件): 测试页面包确保相应的前端组件能良好集成。 - 页面包之外的 Vue 应用: 将 Vue 应用作为整体测试确保相应的前端组件能良好集成。
集成测试中应该模拟什么
- HAML 视图(使用 fixture 替代): 渲染 HAML 视图需要包含运行中数据库的 Rails 环境,这在前端测试中无法依赖。
- 所有服务器请求: 与单元和组件测试类似,运行组件测试时,后端可能无法访问,因此所有出站请求必须被模拟。
- 在页面上不可见的异步后台操作: 影响页面的后台操作必须在此级别测试。 所有其他后台操作无法停止或等待,因此它们会在后续测试中继续运行并产生副作用。
集成测试中不应该模拟什么
- DOM: 在真实 DOM 上测试确保你的组件在预期环境中工作。 DOM 测试的一部分委托给 跨浏览器测试。
- 组件的属性或状态: 在此级别,所有测试只能执行用户会执行的操作。 例如:要改变组件的状态,会触发点击事件。
- Vuex stores: 当将页面的前端代码作为整体测试时,Vue 组件和 Vuex stores 之间的交互也被覆盖。
关于控制器测试
GitLab 正在 从控制器规范(controller specs)过渡到请求规范(request specs)。
在理想情况下,控制器应该很薄。然而,当情况并非如此时,编写一个不包含 JavaScript 的系统或功能测试而不是控制器测试是可以接受的。测试臃肿的控制器通常涉及大量桩设置,例如:
controller.instance_variable_set(:@user, user)并使用 在 Rails 5 中已弃用 的方法。
系统级白盒测试(以前称为系统/功能测试)
正式定义:
这类测试确保 GitLab Rails 应用(例如 gitlab-foss/gitlab)从浏览器角度看能按预期工作。
请注意:
- 仍然需要了解应用程序的内部结构
- 测试所需的数据通常直接使用 RSpec factories 创建
- 期望通常设置在数据库或对象状态上
这些测试仅应在以下情况使用:
- 被测试的功能/组件很小
- 需要测试对象/数据库的内部状态
- 无法在更低级别测试
例如,要测试给定页面的面包屑,编写系统测试是合理的,因为它是一个小组件,无法在单元或控制器级别测试。
只测试正常路径,但确保为任何无法通过更好的测试在更低级别捕获的回归添加测试用例(例如,如果发现回归,应在可能的最低级别添加回归测试)。
| 测试路径 | 测试引擎 | 说明 |
|---|---|---|
spec/features/ |
Capybara + RSpec | 如果你的测试有 :js 元数据,浏览器驱动是 Selenium,否则使用 RackTest。 |
前端功能测试
与 前端集成测试 相比,功能测试对真实后端发出请求,而不是使用 fixture。 这也意味着执行数据库查询,这使得此类测试显著变慢。
另请参见:
- RSpec 测试指南。
- 测试最佳实践 中的系统/功能测试。
graph RL
plain[普通 JavaScript];
Vue[Vue 组件];
feature-flags[功能开关];
license-checks[许可证检查];
plain---Vuex;
plain---GraphQL;
Vue---plain;
Vue---Vuex;
Vue---GraphQL;
browser---plain;
browser---Vue;
plain---backend;
Vuex---backend;
GraphQL---backend;
Vue---backend;
backend---database;
backend---feature-flags;
backend---license-checks;
class backend tested;
class plain tested;
class Vue tested;
class Vuex tested;
class GraphQL tested;
class browser tested;
linkStyle 0,1,2,3,4,5,6,7,8,9,10 stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
classDef label stroke-width:0;
classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
subgraph " "
tested;
mocked;
class tested tested;
end
何时使用功能测试
- 需要后端且无法使用 fixture 测试的用例。
- 不是页面包一部分,而是全局定义的行为。
相关说明
添加 :js 标志以确保加载完整环境:
scenario '成功', :js do
sign_in(create(:admin))
end每个测试的步骤使用 (capybara 方法) 编写。
XHR (XMLHttpRequest) 调用可能需要在步骤之间使用 wait_for_requests,例如:
find('.form-control').native.send_keys(:enter)
wait_for_requests
expect(page).not_to have_selector('.card')考虑不编写系统测试
如果我们确信低级别组件工作良好(如果我们有足够的单元和集成测试,我们应该确信),我们就不需要在系统测试级别重复它们的彻底测试。
添加测试很容易,但删除或改进测试要困难得多,因此我们应该注意不要引入太多(缓慢且重复的)测试。
我们应该遵循这些最佳实践的原因如下:
- 系统测试运行缓慢,因为它们在无头浏览器中启动整个应用程序栈,当集成 JavaScript 驱动时更慢
- 当系统测试使用 JavaScript 驱动运行时,测试在与应用程序不同的线程中运行。这意味着它们不共享数据库连接,你的测试必须提交事务,以便运行的应用程序能看到数据(反之亦然)。在这种情况下,我们需要在每个规范后截断数据库,而不是回滚事务(这是其他类型测试使用的更快策略)。然而,这比事务慢,因此我们只在必要时使用截断。
系统级黑盒测试,也称为端到端测试
正式定义:
GitLab 由 多个组件 组成,如 GitLab Shell、GitLab Workhorse、 Gitaly、GitLab Pages、GitLab Runner,以及 GitLab Rails。所有这些组件 都由 Omnibus GitLab 配置和打包。
QA 框架和实例级场景是 GitLab Rails 的一部分,以便它们始终与代码库(特别是视图)保持同步。
请注意:
- 不需要了解应用程序的内部结构
- 测试所需的数据只能通过 GUI 或 API 创建
- 只能对浏览器页面和 API 响应设置期望
每个新功能都应附带 测试计划。
| 测试路径 | 测试引擎 | 说明 |
|---|---|---|
qa/qa/specs/features/ |
Capybara + RSpec + 自定义 QA 框架 | 测试应放在相应的 产品类别 下 |
更多信息请参见 端到端测试。
请注意 qa/spec 包含 QA 框架本身的单元测试,不要与应用的 单元测试 或 端到端测试 混淆。
冒烟测试
冒烟测试是快速测试,可在任何时间运行(特别是在预部署迁移之后)。
这些测试针对 UI 运行,确保基本功能正常工作。
更多信息请参见 冒烟测试。
GitLab QA 编排器
GitLab QA 编排器 是一个工具,它通过为给定版本的 GitLab Rails 构建 Docker 镜像并对其运行端到端测试(使用 Capybara),来测试所有这些组件是否能良好集成。
在 GitLab QA 编排器 README 中了解更多信息。
EE 特定测试
EE 特定测试遵循相同的组织结构,但在 ee/spec 文件夹下。
如何在正确的级别进行测试?
生活中的许多事情一样,决定在每个测试级别测试什么是权衡的结果:
- 单元测试通常很便宜,你应该将它们视为你房子的地下室:你需要它们来确信你的代码行为正确。然而,如果你只运行单元测试而没有集成/系统测试,你可能会错过大局!
- 集成测试成本稍高,但不要滥用它们。系统测试通常比模拟大量内部细节的集成测试更好。
- 系统测试成本较高(相对于单元测试),如果需要 JavaScript 驱动则更高。请确保遵循 速度 部分的指南。
另一种看待它的方式是思考"测试成本",这在这篇文章中有很好的解释,基本思想是测试成本包括:
- 编写测试所需的时间
- 每次测试套件运行时运行测试所需的时间
- 理解测试所需的时间
- 如果测试失败且底层代码正常,修复测试所需的时间
- 可能的,使代码可测试所需的时间
前端相关测试
在某些情况下,你测试的行为不值得运行完整应用程序的时间,例如,如果你正在测试样式、动画、边缘情况或不涉及后端的小操作,你应该使用 前端集成测试 编写集成测试。