软体测试 — 如何写出好维护的测试?

2025年7月23日

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

先前我们谈了测试金字塔的概念 ,在这一期我们将延续这个主题,进一步来谈如何写出好测试的程式码,以及可以如何让测试更好维护。

如何让测试好维护

在谈完如何写出好测试的程式码,接着我们来谈如何让测试比较好维护。

测试的意图

首先,要让测试好维护,要尽可能避免测试的意图不明,让人读不懂该测试到底在测什么。在业界中有个观点是,好的测试就像文件一样,让人可以透过测试来了解程式码库。从这角度来看,当测试无法让人轻易看懂,该测试就相对没有价值。

在写测试时,有一个叫 DAMP (Descriptive And Meaningful Phrases) 的原则,是指测试要尽可能做到具描述性,用有意义的字句来提高意图的可读性。当这样写,未来的维护者看到后,就能够用更短的时间看懂,这样要维护起来也更容易。

避免重复

除了意图明确的 DAMP 原则,跟写程式一样,如果写测试时有照着 DRY (Don't Repeat Yourself) 的原则,尽可能避免重复,会让测试更容易维护。举例来说,在 [直播] 如何写出更干净、好维护的程式码? 直播中,我们有谈到可以如何运用工厂方法 (factory Method) 来生成测试资料,这样让写测试资料时,能够避免写很多重复,然后要改时要到各处去改 (对这个概念不熟的读者,推荐回去看该直播的回放)。

另一个常见的避免重复方式,是在写 E2E 测试时,可以把重复会有的流程给抽出来,例如前面的登入后到首页的流程,可以抽出来。这样在写不同流程的测试时,就可以直接引入「登入后进到首页」的流程,不用重复写,而且如果「登入后进到首页」的流程需要更改,也可以在一个地方改就好,不用四处去改,这样维护起来也会更容易。

整理分区

除了上述两点外,一个可以简单让测试好维护的方式,是有效做好整理与区分。

举例来说,在写单元测试时,可以透过共置 (colocation),让测试好被维护。共置的概念在于,测试应该要跟程式码放在一起。有些团队会把单元测试放在 /src/test 底下,不论是哪段程式码的单元测试,通通放在那。但比起这种作法,会更推荐把单元测试跟程式码放在同一个资料夹底下,例如 /add/index.ts/add/index.test.ts 都是放在 /add 底下。

当这样整理的好处在于,如果今天想要透过测试来理解某段程式码,因为在同一个资料夹,所以可以马上找到该程式码的测试。同样地,如果更新完程式码,也可以马上找到测试的档案然后更新;如果要删除程式码,要同时把测试删除时,这样放也很容易一起删掉。

当然你可能会问,整合测试与 E2E 测试是不是不适合这样? 毕竟有多个模组都跟测试有关,那应该放在哪里? 因此会推荐在整合测试或 E2E 测试,可以用领域导向 (domain-oriented) 来分类。

举例来说,如果在一个电商相关的 E2E 测试中,我们可以依照领域来分类,例如把商品陈列、订单管理、金流页面等,分在不同的资料夹,然后把相关的流程都放在一起,例如商品页相关的流程的测试都放一起。这样做未来要维护时,就可以很轻松找在相对应的领域,找到相关的测试。

以下是个具体的例子

│   │   ├── product/
│   │   │   ├── productListing.spec.js
│   │   │   ├── productDetail.spec.js
│   │   │   └── addToCart.spec.js
│   │   ├── order/
│   │   │   ├── createOrder.spec.js
│   │   │   ├── orderConfirmation.spec.js
│   │   │   └── orderHistory.spec.js

测试该覆盖哪些案例?

在这期主题文的最后,想与大家讨论一个,相信很多人都有的问题,那就是测试案例该怎么写? 该覆盖哪些?

举例来说,假如今天写了一个叫 sortNumbersAscending 的函式,会把一个带有数字的阵列,转换成升幂排序。当看到这个函式,大家会写哪些测试案例呢?

一个最直观的方式,会是写像是 expect(sortNumbersAscending([3, 1, 2])).toEqual([1, 2, 3]) 这样的测试案例。这种写法是叫以范例为基础的测试 (example-based testing),但这种写法无法完整给开发者信心。以上面这个例子来说,即使通过了,我们能确保在 [3, 1, 2] 的测试案例下,这个函式没问题,但这不代表在其他案例也没问题。

以 JavaScript 来说,假如团队有某个对 JavaScript 不熟的开发者,在写 sortNumbersAscending 时,是这样实作 (这边是举例说明,相信大家不会这样写)

function sortNumbersAscending(arr) {
  return arr.sort();
}

在这个写法下,因为 JavaScript 的语言特性,虽然 sortNumbersAscending([3, 1, 2] 会有正确的输出,但是 sortNumbersAscending([10, 2, 1]) 会输出 [ 1, 10, 2 ],会是不符合预期的。

因为 JavaScript 的 sort 在没有传入比较的回呼函式时,会把阵列中的值转换成字串来比较,才会出现这结果。但假如在写测试案例的范例时,没有去想到 sortNumbersAscending([10, 2, 1]),那很可能即使全部测试案例都通过,但却没有抓到程式码有问题的地方。

不过,实务上来看,很可能无法想到各种要测的极端状况,那可以怎么做来确保有完整覆盖该覆盖的案例呢?

我们在 E+ 成长计划的主题文,会进一步谈透过以特性为基础的测试(Property-based testing) 方式,来确保测试的覆盖率足够完整。

阅读更多

如果你对于「软体测试」这主题感兴趣,我们在 E+ 有写更深入详细的内容,包含测试金字塔 (Testing Pyramid)、测试金杯 (Testing Trophy)、整合测试、E2E 测试等主题。有兴趣的读者,欢迎加入 E+ 成长计划。

本文为 E+ 成长计划的深度内容,截取前三分之一开放免费阅读。欢迎加入 E+ 成长计划阅读完整版本 (点此了解 E+ 的详细介绍)

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