在 GitLab CI/CD 等自動化工具出現之前,軟體開發生命週期(SDLC)中許多環節仰賴手動操作,效率低落且容易出錯。以建置 Java 應用程式為例,開發者需要手動編譯程式碼,管理依賴性,並執行各種測試,這些步驟不僅耗時,也容易因人為疏失導致問題。過去,驗證程式碼的過程同樣繁瑣,開發者需要手動執行單元測試、整合測試和使用者測試,確保程式碼的功能正確性以及不同模組間的協作是否順暢。此外,效能測試和負載測試也需要大量的手動設定和執行,才能評估應用程式在不同負載下的表現。這些手動流程不僅增加了開發成本,也拉長了軟體交付週期。而 GitLab CI/CD 管線的出現,正是為瞭解決這些痛點,透過自動化這些流程,大幅提升軟體開發的效率和可靠性。
開發自動化與持續整合:從Git開始
在這部分,我們將探討為什麼在GitLab出現之前,軟體開發生命週期(SDLC)是如此緩慢且容易出錯。這將幫助你理解GitLab所解決的問題。我們也會介紹Git版本控制系統的基礎知識,並深入瞭解GitLab的基本概念和元件。最後,我們將初步接觸GitLab CI/CD管道,這是本文後續大部分內容的重點。
這一部分包括以下章節:
- 第1章:理解DevOps之前的世界
- 第2章:實踐基本的Git命令
- 第3章:理解GitLab元件
- 第4章:理解GitLab CI/CD管道結構
DevOps之前的世界
為了欣賞GitLab CI/CD管道和DevOps軟體開發方法的強大之處,我們必須瞭解在像GitLab這樣的工具出現之前,軟體是如何構建的。雖然在本章中你不會學到任何實際操作的技能,但你會瞭解GitLab CI/CD管道所成長的世界,並清楚地看到它們解決了哪些問題。掌握這些知識將有助於你理解為什麼GitLab CI/CD管道以這種方式執行,並讓你看到它們為軟體開發生命週期帶來的驚人力量。
簡言之,最好的方式是瞭解過去有多糟糕,才能理解現在有多好!
本章將介紹一個虛構但現實的網路應用程式「Hats for Cats+」,它銷售——你猜對了——給貓咪戴的帽子。你將快速瞭解從一個想法將Hats for Cats轉變為一個經過良好撰寫、測試和佈署的網路應用程式所涉及的內容。你將看到在沒有GitLab CI/CD管道的世界裡,這些任務必須如何完成,以便當你在後續章節中學習到GitLab時,其優勢更加明顯。
在本章中,我們將涵蓋以下主要主題:
- 介紹Hats for Cats網路應用程式
- 手動構建和驗證程式碼
- 手動進行安全測試
- 手動封裝和佈署程式碼
- 手動軟體開發生命週期實踐中的問題
- 使用DevOps解決問題
Hats for Cats網路應用程式簡介
Hats for Cats是一個虛構的網路應用程式,專門銷售棒球帽、牛仔帽和高帽給你最喜愛的毛茸茸朋友。想象一下,它是一個標準的線上商店,就像你用過的成百上千個商店之一。它允許人們瀏覽帽子目錄、將商品放入購物車並輸入賬單和配送資訊。
Hats for Cats的使用者經驗或圖形設計對於本文來說並不重要。它所根據的網路應用程式框架也不重要。甚至它所使用的電腦語言也不重要。玄貓再次強調這一點是因為這是一個重要但可能令人驚訝的觀點:本文是語言無關性的。它將包含多種電腦語言的範例,以增加至少一些範例使用你熟悉語言的機會。但是無論你的應用程式——或Hats for Cats網路應用程式——是用Java、JavaScript、Python、Ruby或其他任何語言編寫的都不重要。
重要的是確保程式碼品質高、行為如預期、安全、效能良好、合理封裝並佈署到正確環境中的一般步驟。本文重點介紹GitLab CI/CD管道如何使軟體開發生命週期(SDLC)中的各種步驟更容易、更快速和更可靠。
我們假設所有編碼都在幕後進行,然後展示如何構建、驗證、保護、封裝和釋出該程式碼。
手動構建與驗證程式碼
在GitLab CI/CD管道出現之前,你需要手動構建和驗證程式碼。這通常是一種可怕而令人心碎的人體驗,原因我們將在這裡討論。
手動構建程式碼
構建程式碼取決於你使用哪種語言。如果你使用的是像Python或Ruby這樣的解釋型語言,那麼構建可能根本不必要。但如果你使用的是編譯型語言,那麼你需要透過編譯其原始碼來構建應用程式。
想像一下你正在使用Java
以下是編譯Java原始碼為可執行Java類別的一些不同方式:
- 你可以使用隨Java開發工具包(JDK)捆綁提供的javac Java編譯器
- 你可以使用Maven構建工具
- 你可以使用Gradle構建工具
有很多理由讓人認為這個手動構建過程是一項繁瑣、令人厭煩且大多數開發人員願意放棄的人工任務:
- 容易出錯:有多少次忘記指示javac指向類別檔案所在的頂級包而不是單個類別檔案?
- 慢速執行時間從幾秒鐘到幾分鐘不等,取決於應用程式大小。
- 容易被遺忘造成混亂當意外執行舊程式碼時不會像預期那樣行為。
- 編寫不良程式碼可能無法編譯造成浪費時間由於構建工程師發送修補程式到開發者等待修補到來。
假如沒有自動化工具?
假設沒有像Maven或Gradle等自動化工具來幫助管理Java專案依賴性並進行編譯?每次都要手動管理這些細節會變得非常繁瑣且容易出錯。
假設有一個大型Java專案需要編譯數百個類別。每次修改其中一個類別後都需要重新編譯整個專案。
// 假設有一個簡單的Java專案結構
// src/com/example/Main.java
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
// 建立目錄結構
mkdir -p src/com/example
// 建立Main.java檔案
cat <<EOF > src/com/example/Main.java
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
EOF
// 編譯Java檔案
javac src/com/example/Main.java -d bin
// 執行編譯後產生之class檔案
java -cp bin com.example.Main
內容解密:
此段落展示瞭如何手動編譯一個簡單Java專案:首先建立目錄結構並生成Java原始檔。
然後使用javac
命令進行編譯並指定輸出目錄。
最後執行生成之class檔案以確認其正常運作。
玄貓注意:在此範例中我們展示了根據命令列之作業方式,
當然也可以透過整合式開發環境(IDE)如IntelliJ IDEA或Eclipse來簡化此流程,
然而此處我們專注於理解底層原理及作業邏輯。
手動驗證程式碼
完成程式碼編譯後需進行驗證確保其正確執行。測試具有無數形態且種類別繁多, 以下列舉一些常見形式:
- 單元測試(Unit Tests):檢查單個函式或方法功能是否正常。
- 整合測試(Integration Tests):檢查不同模組或服務之間互動功能是否正常。
- 系統測試(System Tests):檢查完整系統功能是否如預期執行。
- 效能測試(Performance Tests):檢查系統在高負載下之表現。
- 安全測試(Security Tests):檢查系統之安全性及漏洞。
假如沒有自動化工具?
假設沒有像JUnit或TestNG等單元測試框架來幫助管理和執行測試?每次修改程式碼後都需要手動執行測試來確保沒有引入新問題。
// 假設有一個簡單JUnit測試範例
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MainTest {
@Test
public void testMain() {
Main main = new Main();
String result = main.getMessage();
assertEquals("Hello, world!", result);
}
}
內容解密:
此段落展示了一個簡單JUnit測試範例:
首先定義MainTest
類別並使用@Test
註解標記測試方法。
接著呼叫被測方法getMessage()
並利用assertEquals()
進行結果驗證。
玄貓提示:在此範例中我們展示了根據註解及assert機制之JUnit框架,
當然也可以透過其他框架如TestNG進行類別似操作,
然而此處我們專注於理解底層原理及作業邏輯。
自動化工具之好處
透過引入自動化工具如Maven、Gradle及JUnit可以顯著提升開發效率及降低人為失誤風險:
- 統一管理依賴性:自動化工具可統一管理專案的依賴性及版本, 避免因為依賴性衝突導致開發環境不穩定。
- 自動化編譯與封裝:自動化工具可自動化編譯與封裝流程, 避免因為人為失誤導致無法成功編譯。
- 整合測試框架:自動化工具可與測試框架整合, 自動化執行各種形式之測試以確保程式碼品質。
機能測試
你的程式是否能夠正確運作?這是機能測試所要解答的問題。大多數的程式設計專案都會從一份規格書開始,這份規格書描述了軟體應該如何執行:給定特定的輸入,應該提供什麼樣的輸出?開發者的工作在於確保他們編寫的程式碼符合這些規格。那麼,他們如何確認程式碼符合這些規格呢?這就是機能測試的用途。
正如一般測試有多種形式,機能測試也包含了許多子類別。以下是一些常見的機能測試型別:
正常路徑測試(Happy Path Testing)
正常路徑測試確保當軟體接受常見、有效的輸入時,能夠正常執行。例如,如果你在電腦上輸入 2 + 2
,它應該傳回 4
!正常路徑測試似乎是最重要的一種測試,因為它們檢查的是使用者最有可能遇到的行為。但事實上,你通常只需要幾個正常路徑測試來覆寫大部分的常見使用案例。而那些覆寫不尋常或意外情況的測試則通常會更多。
邊緣案例測試(Edge-Case Testing)
邊緣案例測試確保輸入值在可接受範圍的極端情況下,不會破壞軟體。舉例來說,電腦使用者更有可能輸入 56 ÷ 209
(這些值在電腦可接受範圍的中間)而不是 0 + 0
或 999,999 – 999,999
(這些值在範圍的邊緣)。邊緣案例測試確保這些極端情況不會使軟體失效。
隅角案例測試(Corner-Case Testing)
隅角案例測試確保軟體能夠處理兩個或多個同時發生的邊緣案例。想象一下,你的銀行應用程式是否允許你安排在未來最遠期限提取最小有效貨幣單位?如果你的軟體接受三個、十個或一百個同時輸入值,你需要確保它在所有輸入都推到極端值時仍能正常執行。
悲觀路徑測試(Unhappy Path Testing)
悲觀路徑測試確保軟體在接收到無效或異常資料時,能夠正確處理。例如,你需要確保你的電腦在被要求除以零時不會當機。你必須檢查銀行應用程式在你要求提取負六美元時不會意外給你存款。而你的貨幣轉換軟體應該在你詢問二月三十一日匯率時給出合理的錯誤訊息。
手動建立與驗證程式碼
由於進入應用程式中的壞資料方式比好的資料多,開發者經常集中在正確處理預期資料上,但忽略了意外、異常或超出範圍的資料型別。程式需要預見並優雅地處理各種資料——無論好壞。撰寫完整的一組正常和悲觀路徑測試是確保開發者編寫的程式碼無論使用者輸入什麼資料都能良好運作最好的方法。
除了涉及有效和無效資料的一些行為外,還有一個維度可以用來分類別測試:目標程式碼塊的大小。
單元測試(Unit Testing)
單元測試是針對單一方法或函式進行測試。例如,你可能想要測試一個名為 alphabetize
的函式,它接受任意數量的字串作為輸入並傳回按字母順序排序後的字串。要測試這個函式,你可能會使用單元測試。
單元測試可以從不同角度覆寫該函式:
- 一些可能覆寫正常路徑。例如,它們可以傳遞
dog
、cat
和mouse
字串作為輸入。 - 一些可能覆寫邊緣或隅角案例。例如,它們可以傳遞一個空字串、僅由數字組成的字串或已經按字母順序排序的字串。
- 一些可能覆寫悲觀路徑。例如,它們可以傳遞意外的資料型別(如布林值)而不是預期的字串型別。
整合測試(Integration Testing)
整合測試不僅僅檢查單一函式,而是檢查一組函式如何相互作用。例如,假設你的貨幣轉換應用程式有四個函式:
get_input
:從使用者那裡接受輸入,以來源貨幣、來源金額和目標貨幣形式。convert
:將來源貨幣金額轉換成目標貨幣。print_output
:告訴使用者轉換產生多少目標貨幣。main
:應用程式的主要進入點。當應用程式被使用時呼叫此函式。它呼叫其他三個函式並將每個函式的輸出作為下一個函式的輸入。
要確保這些函式之間良好協作——即檢查它們是否能夠良好整合——你需要整合測試呼叫 main
函式,而不是單元測試呼叫 get_input
、convert
和 print_output
函式。這讓你能夠在更高層次上進行測試,也就是說接近真實使用者使用應用程式的方式。
此圖示
graph TD A[main] --> B[get_input] B --> C[convert] C --> D[print_output]
此圖示展示了主流程與每個步驟之間相互關聯與互動方式。
內容解密:
- 在此圖示中展示的是主要流程以及每個步驟之間相互關聯與互動方式。
- 我們可以看到
main
函式呼叫get_input
函式取得使用者輸入。 - 接著呼叫
convert
函式進行貨幣轉換。 - 最終呼叫
print_output
函式顯示轉換結果給使用者。 - 此流程展示了整合測試如何檢查不同功能模組之間是否能夠良好協作與整合。
副標題(待補)
自動化測試在DevOps時代的演變
在DevOps時代之前,軟體開發團隊通常會面臨一個顯著的挑戰:如何確保各個功能模組能夠無縫整合。傳統上,開發人員會將主要功能模組分割成多個較小的子函式,然後透過主函式來協調這些子函式之間的互動。這種方法雖然簡單,但卻容易忽略函式之間的協同問題。因此,單元測試(Unit Tests)無法完全發現這類別問題,而整合測試(Integration Tests)則能夠更有效地揭示這些問題。
測試人員通常會將各種測試方法視為一個金字塔結構。在這個模型中,單元測試佔據金字塔的寬廣基底:它們是低層次的,因為它們測試的是基本的程式碼片段,且數量眾多。整合測試則位於金字塔的中間層:它們運作在比單元測試更高的抽象層次,且數量較少。而在金字塔頂端的是使用者測試(User Tests),這是我們接下來要討論的第三類別測試。
使用者測試模擬使用者的行為,並以與使用者相同的方式操作軟體。例如,如果使用者透過輸入源貨幣、金額和目標貨幣來使用外匯應用程式,並期望看到目標貨幣的金額作為輸出,那麼使用者測試會做相同的操作。這可能意味著它使用應用程式的圖形使用者介面(GUI)來點選按鈕和輸入欄位中的值。或者,它可能透過呼叫應用程式的REST API端點,傳入輸入值並檢查結果以取得輸出值。然而,無論是哪種方式,使用者測試都會與應用程式進行互動,並盡可能模擬真實使用者的行為。同樣地,使用者測試可以包括正常路徑測試、邊界情況和異常路徑測試,以涵蓋軟體規範中描述的所有情境,以及任何其他測試設計師能夠想到的情境。
手動構建與驗證
至今為止,我們已經解釋了單元、整合和使用者測試的不同目的,但我們尚未描述另一個基本差異。單元和整合測試幾乎總是自動化的;它們是驗證其他電腦程式碼的一個電腦程式碼。然而,盡管在可能的情況下自動化了使用者測試,由於可靠地、可重複地編寫與應用程式GUI互動的測試存在足夠多的困難,因此許多使用者測試必須手動執行而不是自動化執行。網頁應用程式特別難以進行測試,因為其載入時間、頁面顯示、缺失或未完全載入CSS檔案以及網路擁塞等不確定行為。這意味著雖然軟體開發團隊經常嘗試自動化網頁應用程式的使用者測試,但最終他們通常得到了自動化和手動使用者測試的一種混合形式。如你所料想到的一樣,手動執行使用者測試非常昂貴且耗時。
效能測試
完成功能性測試之後,你可能會認為我們已經涵蓋了所有必要的基礎專案。然而我們才剛剛開始。另一個需要考慮的應用特性是其效能:它是否能夠快速執行以避免讓使用者感到挫折?它是否符合開發人員在開始編寫程式碼之前收到的效能規範?其效能是否比競爭對手更好或更差?這些都是效能測試旨在回答的一些問題。
設計和執行效能測試通常具有挑戰性。在評估應用執行速度時需要考慮許多變數:
- 應該在什麼環境下執行呢?建立與生產環境相同的一個環境通常成本過高;但我們可以在效能測試環境中削減哪些角落而不會嚴重偏離效能結果呢?
- 效能測試合應使用什麼輸入值?根據應用程式的不同特性某些輸入值可能需要更長時間來處理。
- 如果應用程式是可組態的那麼我們應該使用什麼組態設定呢?這對於沒有標準組態且大多數使用者都設定相同組態的一個應用程式尤其重要。
即使你可以設計有意義且有效地進行效能測試;它們通常需要花費大量時間來執行而且有時候會產生不一致結果。這促使團隊頻繁重新執行效能測試從而導致時間消耗增加。
負載測試
效能測試有一個密切相關型別稱為負載測試(Load Tests)。儘管效能測試決定您軟體完成單一操作時執行速度(例如單一貨幣轉換、單一銀行存款或單一算術問題等),但是負載測試則評估您應用程式同時處理數百或數千名使用者時執行表現如何。
這種型別測試由於設計問題類別似於效能測試並且可產生同樣不穩定結果;但是設定負載測試可能還更昂貴因為它們需要一種方式來模擬數百或數千名同時操作您應用的人群。
手動構建與驗證程式碼
到目前為止,我們已經解釋了單元、整合和使用者測試各自不同目的;但是我們並沒有解釋另一個基本差異——單元和整合測試幾乎總是自動化:它們是透過電腦程式來檢查其他電腦程式。 然而儘管每當可能就儘量將其自動化處理;編寫可靠且可重複執行與應用GUI進行互動處理過程較為困難;這使得許多此類別測試必須透過手動而非機器執行。 Web應用通常難於進行驗證因為載入時間不確定行為,頁面未完全呈現,缺少或者CSS檔案未完整載入以及網路阻塞等原因; 這意味著雖然軟體開發團隊常常嘗試對web應用進行自動化執行驗證過程;它們往往最終會得到一些結合了自動化和手工驗證過程。 如您猜測到一樣;手工進行驗證會耗費大量時間與資源;並且也會影響工程師士氣;
內容解密:
上述段落詳細解說了DevOps時代之前軟體開發團隊面臨的挑戰以及不同型別檢驗方法之間的關係。
- 函式整合:傳統上開發人員會將主要功能模組分割成多個較小子函式並由主函式協調其互動。然而這種做法容易忽略各函式之間協作問題。
- 金字塔模型:單元、整合和使用者檢驗分別佔據金字塔底層、中層及頂層:
- 單元檢驗:專注於基礎程式碼片段;低層次且數量多
- 整合檢驗:於高層次運作;操作於比單元檢驗更複雜階段
- 使用者檢驗:模擬真實使用者行為;確保與真實情況相似
- 演示示例:提供外匯轉換功能來說明使用者檢驗如何操作
效能與負載檢驗
接著解釋了效能檢驗及負載檢驗:
- 目的:確認系統運作速度符合需求及規範
- 困難處:設計及執行上均存在挑戰
- 環境設定:生產環境相同但昂貴
- 輸入值選擇:不同輸入值處理時間不同
- 組態設定:複雜可變
- 負載檢驗:進一步評估系統同時處理大量使用者時表現