iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0

前面的章節教過用 Git 可以把程式碼「儲存版本」,這樣加新功能時,如果發現舊功能壞掉,可以倒回去重來。不過這裡有另外一個問題,就是「我們如何發現舊的功能有沒有壞掉?」

當我們的程式功能很少時,還可以列一張清單,照著清單上的項目一樣一樣手動用用看,確定有沒有壞掉。

但是比較有規模的程式常常有幾十種功能,每種功能又都要確認正確的操作會拿到正確的結果,而且連錯誤的操作,應該要拿到的錯誤畫面我們也要測試。這樣清單上可能有好幾百條操作,照著點完都天黑了。

那麼職業的開發者是怎麼做的呢?答案就是使用 「自動化測試」 。這個詞聽起來很厲害,但是概念卻十分簡單,就是每次寫好一隻重要功能的函式時,同時也會寫出幾隻呼叫這個函式,然後檢查這個函式結果是否正確的測試函式

這種針對函式進行的測試,我們叫它們單元測試

我們用 Python 目前主流的測試工具 pytest 來示範。

pytest

首先在空資料夾裡,用 uv init 新建專案,並在我們的專案設定檔 pyproject.toml 中,加入以下的設定:

[dependency-groups]
dev = [
    "pytest>=8.4.2",
]

[tool.pytest.ini_options]
pythonpath = "src"
addopts = [
    "--import-mode=importlib",
]

接著用編輯器開啟 src/app.py,這是放我們的程式的地方,我們來實做一個加法函式:

def add_numbers(a, b):
    """將兩個數字相加,如果大於零回傳結果,否則回傳零。"""
    result = a + b
    return result if result > 0 else 0

接著開啟 test/test_app.py 檔案。測試檔的慣例就是會用 test_要測的檔案名.py 做為名稱。在這個檔案中,我們寫入以下測試函式:

from app import add_numbers

def test_add_numbers_positive():
    assert add_numbers(2, 3) == 5

def test_add_numbers_negative():
    assert add_numbers(-1, 0) == 0

這個測試檔案先匯入 (import) 了我們實作的函式,並且定義了兩個函式,在呼叫我們寫的add_number 時,傳入不同的參數。第一個是正常的情況,我們叫它 Happy path,傳入 23 當參數,並用 assert 確認它應該要等於 5。第二個則是 Sad path,傳入 0-1,確認它應該回傳 0

最後打開終端機,cd到專案的資料夾中,並執行 uv run pytest,就可以看到兩個測試都通過了。


沒空寫測試是因為你太晚想要寫測試

這樣一來,每當做出新功能時,都有好好附上自動測試的程式,就像是一張安全網,加上新功能時自動跑一下測試,就可以對之前的東西沒有被改壞比較有信心。如果發現出錯了,也可以馬上動手修改。

而在正式的中大型專案裡,常常會有測試的程式碼比實作功能的程式碼還要多的情況。

有些程式設計師沒有寫測試的習慣,覺得花太多時間,然而他們常常會花更多時間去手動驗證程式有沒有好好運作,或是等到發現某個功能不太正常,但早就不記得是什麼時候改壞的了。


更極限的作法:先寫測試的 TDD

既然附上測試是個好習慣,有一派的人認為我們可以把測試程式當做軟體的規格書。每次都先寫測試程式碼,然後不用實作就先跑測試,這樣當然是會拿到錯誤的紅燈(Red light)的。

接著我們只實做最少,可以通過測試的程式碼,再跑測試,確定通過測試得到綠燈(Green light)。接著再加上更多我們想要的規則到測試裡。如此都是先寫測試 -> 確定失敗 -> 寫程式通過測試 -> 再加更多測試 -> ....。直到我們的想要的功能都在測試檔案好好的描述清楚了,測試也都通過了,我們的程式就算完成了。

這種先寫測試再實作的方法,我們叫它測試驅動開發,英文是 test driven development,大家更常用它的縮寫 TDD 來稱呼。


什麼樣的函式跟程式語言比較好測試

如果有個函式,當我們每次給它一樣的參數輸入,它都保證會回傳一樣的結果,不會有其它副作用時,在程式的理論中我們叫它 純函式。這種純函式是最容易測試的,而上一篇提到的函數式編程就特別喜歡使用純函式,因此很容易寫測試。


不只測函式

除了測試函式的回傳結果正確與否的單元測試之外,還有另一把程式當做不透明的黑盒子,直接用程式碼控制瀏覽器,自動點擊按鈕,並觀察畫面上出現的結果是否正常的測試,我們叫它整合性測試,integration test。

整合性測試常在需要複雜畫面互動的程式中使用。


TDD 與它的歡樂小夥伴們

除了把函式測試碼當做規格之外,後來有一些人,希望能用更像一般語言的方式描述程式的規格。因此衍生出許多不同的流派。如行為驅動開發 BDD,及讓用程式的人與寫程式的人都能用一樣的詞彙來溝通的 領域驅動開發 DDD 等。

前面講 DSL 的章節提過 Ruby 語言寫規格的 RSpec,就是個很不錯的 BDD 工具,懂英文的話,會發現每一句都幾乎是可以唸的英文句子,不懂程式也能猜出要測什麼行為。

RSpec.describe Array do
  element_list = [1, 2, 3]

  subject { element_list.pop }

  it "is memoized across calls (i.e. the block is invoked once)" do
    expect {
      3.times { subject }
    }.to change{ element_list }.from([1, 2, 3]).to([1, 2])
    
    expect(subject).to eq(3)
  end

  it "is not memoized across examples" do
    expect{ subject }.to change{ element_list }.from([1, 2]).to([1])
    expect(subject).to eq(2)
  end
end

最近隨著 AI 興起,還有直接寫規格說明的 規格驅動開發 SDD,但這個就不知道之後會不會發展的更好就是了。(那在更以後的未來,會有什麼 XDD 之類的嗎?)


跟 Github 配合

在我們之前上傳專案的網站,有個功能叫 Github Action,可以設定每次上傳檔案之後,都自動幫你跑專案裡面的測試碼。這是相當進階的技術,是在軟體工程領域裡的持續整合/交付 、CI/CD 這個聽起來超專業的討論範圍了。


營火前的小複習

  • 自動化測試來確保以前寫的功能沒有被弄壞
  • Python 裡面可以用 pytest 來跑測試。(JavaScript 可以用 Jest 或 Vitest,每個語言都有測試工具)
  • 純函式很容易測試,函數式編程很容易測試。
  • 直接操作網頁或軟體的測試叫整合性測試
  • 有個先寫測試的流派叫 TDD。後來還有 BDD 、 DDD 跟 SDD。

地圖


上一篇
Ch 25. 用不同的角度來看待邏輯與資料
系列文
Just enough code with AI: 給新手們的程式設計世界觀27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言