软体测试 — 测试金字塔 (Testing Pyramid)

2025年7月21日

💎 加入 E+ 成長計畫 如果你喜歡我們的內容,歡迎加入 E+,獲得更多深入的軟體前後端內容

软体测试是每个软体工程师都需要掌握的技能,因此我们将陆续来谈这个主题,今天这篇文章会先概要介绍软体测试的测试概览,以及大家常听到的测试金字塔 (Testing Pyramid) 。

什么是测试? 谁该写测试?

测试是软体品质与稳定性的把关,确保软体运行都符合需求,不会出现错误。测试的本质之一是找出错误,还记得在 如何做好 Code Review? 如何写出更快通过 Code Review 的程式码? 一文我们有提到,很多人可能会认为程式码审查是在帮忙找错误,但先前微软的研究指出,程式码审查对协助找出错误的帮助有限。比起程式码审查,测试更能协助找到错误,让软体上线前能把错误都排除,借此把关软体品质的存在。

在早期的软体开发模式中,开发者鲜少需要自己写测试,在那个年代,几乎都是由 QA 团队来负责测试的环节。然而,近年来软体业界的趋势有所改变,越来越多团队发现由开发者负责某部分的测试,对于整体效率、品质都会有所提升。

这与先前在 软体工程师该如何值班 (on-call)? 一文谈到的概念相似,当某个人需要为自己所做的结果负责时,做出的成果品质会比较高,因为如果品质不高,后果不再是由别人帮忙扛,而是自己得负责。测试就是为软体的品质把关,所以当测试是由开发者负责,当程式出问题时,就不能甩锅说是 QA 没测到。当没办法甩锅,需要自己承担时,在写程式就会更加小心谨慎,以免未来的自己要帮现在的自己擦屁股。

虽说现在有不小部分的测试责任,是由开发者自己负责,有些团队仍会有自动化测试工程师,来协助涵盖更广的面向,包含非功能性 (non-functional) 的测试,例如效能、安全性、可用性等面向的测试,甚至是帮忙开发测试使用的模拟伺服器 (mock server)。

测试金字塔 (Testing Pyramid)

前面谈到测试的好处时,有特别强调对于软体稳定性的好处;相信有些读者可能会问,除了稳定性,也有效能测试、安全性测试等不同测试,为什么只强调稳定性呢? 确实如果要广义谈软体测试,有效能、安全性、无障碍 (a11y) 等不同类的测试。

而在本期与在接下来几期的内容,我们会聚焦在讨论稳定性相关的测试,因为这是绝大多数软体工程师都要面对的;其他类的测试可能更专门一些,所以不会在这次系列文的范围中。

具体来说,让我们先谈一个在软体工程师都该知道,而且在面试经常被问到的概念 — 测试金字塔。可以从下图看到,测试金字塔的组成,是由最底下的单元测试 (unit tests),到中间层的整合测试 (integration tests),再到最上层的端到端测试 (E2E tests)。

                   /\\
                  /  \\
                 /    \\
                /      \\
               /        \\
              /   E2E    \\
             /   Tests    \\
            /--------------\\
           /  Integration   \\
          /     Tests        \\
         /--------------------\\
        /       Unit           \\
       /       Tests            \\
      ----------------------------

从测试金字塔的概念来说,至少要有单元测试,而越有余力时,越往上层的整合测试与端到端测试去覆盖。当然,理想的状况下,这些应该都要被覆盖。在《Google 的软体工程之道》一书当中提到,建议是 80-15-5 的分配,单元测试要覆盖至少 80%,而整合测试 15%,以及端到端测试要覆盖主要路径。往下让我们逐一讲解这几种测试是什么。

单元测试 (Unit Tests)

单元测试顾名思义是在测某个单元,而这边的单元通常可以是一个类、一个函式,或者一个元件。举例来说,最简单的单元测试,会像下面这样

// 有一个函式,会把两个数相加
function add(a, b) {
  return a + b;
}

// 单元测试来确保该函式的行为如预期
describe("add function", () => {
  it("should return the sum of two numbers", () => {
    const result = add(2, 3);
    expect(result).toBe(5);
  });
});

单元测试范围会比较小,也因此会比较独立。因为范畴小且独立,所以执行单一的单元测试会比较快,这边指的独立,是指可以单独运行,不需用依赖其他的系统就能跑测试。在跑测试时,也可以把环境切开,可以不需用在生产环境中执行测试,而是在进到生产环境前就把测试跑完。

整合测试 (Integration Tests)

整合测试的范围会比单元测试广一点,通常会是一个子系统,独立性不会像单元测试那么高。很多时候,但一个功能自己运行没问题,但当不同模组整合在一起,就可能会出现不如预期的状况;因此,就会需要同时测试不同模组整合在一起,避免当整合时出问题。

你可能会好奇,怎么会有单元都没问题,整合却出问题的状况? 在软体开发的世界中,这还真的很常发生。举例来说,可能有版本对齐问题 (例如不同服务的版本没有对齐)、可能非预期的副作用 (side effect),这些都是在写单元测试时覆盖不到的。

又或者说,在写单元测试时,经常会用 mock (目前技术社群普遍不翻译这个字,所以这边保持用英文,mock 是指模拟的对象),而这时就可能出现因为写 mock 都没问题,但是在实际跑软体时,模拟的元件却出问题,这时就需要靠整合测试抓出来。

因为需要整合不同的模组,整合测试所需要花费的时间,通常比单元测试来得高。

端到端测试 (E2E Tests)

端到端测试的范围最广,会是以模拟使用者使用系统的方式,走过完整的流程。下面是目前社群中最热门的开源 E2E 测试工具 Playwright 官网中的例子,可以看到就像模拟使用者在用网站时,测试的脚本就一步步模拟使用者的行为、每个行为后预期会发生的事。

import { test, expect } from "@playwright/test";

test("has title", async ({ page }) => {
  // 模拟使用者进到页面
  await page.goto("<https://playwright.dev/>");

  // 预期进到页面后,会看到 Playwright 这个标题
  await expect(page).toHaveTitle(/Playwright/);
});

test("get started link", async ({ page }) => {
  await page.goto("<https://playwright.dev/>");

  // 模拟使用者点击 Get started 字样的连结
  await page.getByRole("link", { name: "Get started" }).click();

  // 预期点完连结进到的页面,会有 Installation 的标题
  await expect(
    page.getByRole("heading", { name: "Installation" })
  ).toBeVisible();
});

因为横跨整个系统的模组,基本上独立性低 (整合性高),执行起来也比较耗费时间。

有读者可能会问,假如我们有单元测试,能覆盖某个单一模组,又有 E2E 测试可以覆盖完整的使用者情境,这样为什么还需要整合测试?

确实直观上来说,E2E 测试本身也会测试到不同模组之间的整合,因为假如整合有问题,使用者的角度来操作就无法顺利使用;但是 E2E 测试本身有一些问题,是让整合测试仍有存在价值的。

其中包含:

  • E2E 测试是走过整个完整流程,所以如果出问题的是外部依赖的模组,那可能会出现团队自己的程式码没问题,但仍无法通过测试的状况。举例来说,假如今天某个聊天机器人,背后是呼叫 OpenAI 的 API,而 OpenAI 的伺服器挂掉,导致 E2E 测试在模拟使用者的情况出现测试案例没通过,但这时因为不是自身程式码的问题,就会让测试相对不稳 (俗称有 flakiness),导致测试案例没过,但没办法透过修改程式码让测试案例跑过。但假如是整合测试,可以去模拟对外部的依赖,来确保程式码的整合是没问题的。
  • E2E 测试要测某些整合的状况会相对困难,举例来说,假如今天我们想测试一个状况是 A 模组先呼叫后呼叫 B 模组才可行,而 B 模组先呼叫后才呼叫 A 模组则会进到某个错误处理的情境。从 E2E 测试的角度来看,这种状况很难被测到,因为 E2E 是模拟使用者操作,所以一般都是照正常情境的 A 模组先呼叫,可能只有在极少数出问题时才会变成 B 模组先呼叫,这样可能要跑非常多次测试才会出现一次异常,从测试角度来说很没效率。但假如是整合测试,就可以直接去模拟 B 模组先呼叫的情境,借此来测试错误处理的状况。

因为上述的原因,以及前面提到的 E2E 测试跑起来比较耗时,让整合测试仍有其存在的价值,也因此有「推荐多写整合测试」这个说法。

如果想更了解 E2E 测试,推荐阅读《软体测试 — E2E 测试是什么? 跟整合测试有什么区别? 》一文。

对测试金字塔的反思

虽然测试金字塔是目前社群中比较广为人知的,但是也有不少观点不完全认同。在软体测试领域活跃的开发者 Kent C. Dodds 就提出过不同观点的测试金杯 (Testing Trophy),推荐有兴趣的读者可以一看这个介绍 (连结)。

Kent C Dodds 提出的测试金杯,图片来自《The Testing Trophy and Testing Classifications》一文
Kent C Dodds 提出的测试金杯,图片来自《The Testing Trophy and Testing Classifications》一文

Vercel 创办人也曾发过一个推文,提到他认为整合测试应该要占比最大,他的观点背后的原因,可以在这个推文看到。

阅读更多

以上是关于软体测试的介绍,在 E+ 成长计划中,我们更深入谈软体测试的各面向。对更深入了解这个主题,以及其他前后端开发、软体工程主题感兴趣的读者,欢迎加入 E+ 成长计划一起成长 (连结)。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們