iT邦幫忙

2026 iThome 鐵人賽

DAY 17
2

配合分支:day17/start,Repo: 按我

來寫第一個 Android Unit Test 吧!Unit Test 聽起來很高大上啊!今天就來寫!不過,照片日記有用 Android Framework 的東西,例如:ActivityViewModel⋯ 等,不能寫純粹的 Unit Test。還記得前面說 Android 和 Java 的關係難分難捨嗎?要測試含有 Android Framework 提供的 class,就需要跑 Android Unit Test。

特點是需要跑在虛擬機器裡頭,真的 run 起一個 Android 環境來跑這些測試。
聽起來很複雜⋯ 但其實只要將測試檔案放在 app/src/androidTest 的目錄之下,接著照 app/src/main 後面的 App 路徑去建立檔案,Android Studio 就會自動辨認 androidTest 下的檔案是 Android Unit Test 了。

調整功能

這次就來測試「寫入日記」的檢查,寫成 Test。照慣例,先來偷偷的做一點小調整。
先來強化 title 和 note 的判斷,也把使用者多打的空白移除。

可以用 Kotlin 提供的 trim() 方法(文件),把一個字串「前面」和「後面」的空白字元移除。

先來調整這一塊 👇🏼

https://ithelp.ithome.com.tw/upload/images/20260905/20141615c8Pvq3kTpU.png

改成這樣,要寫入被 trim() 後的文字才行。

https://ithelp.ithome.com.tw/upload/images/20260905/20141615AGdM4nqKeo.png

寫好功能之後,準備來寫相關的測試!

撰寫測試

在寫測試以前,要先知道 Unit Test 有一些約定俗成的格式。知道之後再介紹 Unit Test 與測試作業流程有關的方法。

格式

每一個測試最簡單可以分成三個階段

  1. 建立初始狀態或前置條件 -> 就像是做菜前的備料,可能需準備假資料。
  2. 觸發事件或觸發動作 -> 在這裡呼叫要測試的 Method。
  3. 預期的反應,或預期的狀態變更 -> 在這裡驗證,也就是測試是否成功,通常在這邊驗

我在註解裡頭會詳細的將三個階段在做什麼記錄下來,一方便「真的理解在測什麼」,另一方面為了 3 個月後的自己做點好事⋯。

「照片日記」主要用這種風格的註解:

// Given:建立日記狀態物件,並記錄目前已有幾篇日記

// When:從 public saveDiary 傳入兩個都只有空白的欄位

// Then:確認沒有新增日記,並顯示既有的驗證錯誤

這個 Given - When - Then 就是上述提到的三個階段。另一種常用的分三階段的註解是:

// Arrange:建立日記狀態物件,並記錄目前已有幾篇日記

// Act:從 public saveDiary 傳入兩個都只有空白的欄位

// Assert:確認沒有新增日記,並顯示既有的驗證錯誤

基本上大架構是一樣的。

Unit Test 常見的方法

Android Unit Test (或 AndroidTest) 需要有這行 @RunWith(AndroidJUnit4:class)

https://ithelp.ithome.com.tw/upload/images/20260905/201416158U3o6Z4xoW.png

測試寫在 class 的 {} 裡頭。有小老鼠(其實這東西正式名稱是 Annotation)的方法比較特別。像是 @Before@After

@Before
fun whateverNameForIt() {  // 👈🏼 名稱可以自行取名
	// 每次「任何一個」測試「開始以前」,都會執行這裡
}

而 After 則是反過來

@After
fun whateverNameForIt() {  // 👈🏼 名稱可以自行取名
	// 每次「任何一個」測試「執行結束」,都會執行這裡
}

讀者可能會想問,這兩個有啥用處?Before 通常用於準備變數、狀態,把他們變成「最初」的樣子。而 After 則用來清掃狀態。

以「照片日記來說」

https://ithelp.ithome.com.tw/upload/images/20260905/20141615KYc0hmVu3Q.png

這幾個方法就是每一則測試,都會去跑的 Before/After 方法。以及在 AndroidTest 取得 Android Context 的方式,和取得日記檔案的方法。(畢竟要測試 ViewModel 是否「真的有好好的去建立日記檔案」)

測試

https://ithelp.ithome.com.tw/upload/images/20260905/20141615mej4O3XuUY.png

這個「新增日記」頁面,主要有三個想測試的:

  1. 兩個都空白,要顯示錯誤
  2. Title 是空白,可以新增但就是沒有標題
  3. Note 是空白,可以新增,但就是沒有內文

兩個都空白,要顯示錯誤

https://ithelp.ithome.com.tw/upload/images/20260905/20141615fKVfK5D8z8.png

i. 先去取一開始有多少日記
ii. 接著寫入兩個空白的,反正會被 trim 塞個 \n 換行好像蠻調皮的,但應該要可以
iii. 檢查日記大小,應該要不變。然後錯誤訊息要被顯示

Title 是空白,可以新增但就是沒有標題

https://ithelp.ithome.com.tw/upload/images/20260905/20141615lLwZyLYq1o.png

i. 先去取一開始有多少日記
ii. 也是很心機,Title 塞一個 tab \t,內文寫今天下雨故意給他一堆空白
iii. 檢查日記,應該要加一個。然後去檢查最新的日記,應該要有「今天下雨」內文的。

Note 是空白,可以新增,但就是沒有內文

https://ithelp.ithome.com.tw/upload/images/20260905/20141615IPhgdOljjS.png

i. 照樣,取一開始有多少日記
ii. 再次心機,Title 塞一個含有一堆空白的「旅行」,內文留白
iii. 檢查日記,應該要加一個。然後去檢查最新的日記,應該要有「旅行」標題的。

來跑測試吧!

https://ithelp.ithome.com.tw/upload/images/20260905/20141615eVhXa8xDPP.png

https://ithelp.ithome.com.tw/upload/images/20260905/20141615ruxXqCOscZ.png

先在 Android Studio 裡頭,將測試檔案開起來 PhotoDiaryViewModelTest。看到 class 前面的箭頭了嗎?按下去後按:Run PhotoDiaryViewModelTest 選項。

接下來就會開始跑囉!洗咖西,沒想到,居然有錯!
看看結果視窗會有說明,哪邊失敗。可以看到訊息是:

https://ithelp.ithome.com.tw/upload/images/20260905/20141615aO82lh0be6.png

expected:<4> but was:<3>

應該要是 4 則日記,卻只有偵測到 3 則。看來是沒有成功的寫入資料⋯ 🤔

問題出在哪?

體制吧!不是我是說程式碼!其實,在加入去除空白功能的時候,偷偷留下了一些小錯誤來製造本章的節目效果,在判斷「請至少填寫標題或內容」的時候,加入了錯誤的 if 判斷條件!(登登,What!?)

請看:

if (normalizedTitle.isBlank() || normalizedNote.isBlank()) {
    validationError = "請至少填寫標題或內容"
} else {
    // 成功寫入,以下省略...
}

現在這樣會發生什麼事呢?原來 Title 或是 Note 只要有任一是空白的,就會被判斷為錯誤,自然不會被加入到最終結果之中囉⋯⋯

調整方式也很容易,這邊就請讀者自行調整囉。調整結束後再跑 Test,這次應該會全部通過了。

TL;DR

  • Android Unit Test:需要真正跑起 Android 環境的測試,放在 app/src/androidTest 目錄下就會被自動辨認;因為照片日記用到 ActivityViewModel 這些 Android 的東西,所以必須用它才能測。
  • trim():Kotlin 內建的方法,把字串「前面」和「後面」的空白字元拿掉;今天要先 trim 再寫入,使用者手滑多打的空白才不會污染資料。
  • 測試三階段(Given - When - Then):每個測試分成「準備前置狀態 → 觸發動作 → 驗證預期結果」三步,另一種講法叫 Arrange - Act - Assert;寫在註解裡能幫助自己也幫助三個月後的自己看懂在測什麼。
  • Annotation(小老鼠):方法前面那些 @ 開頭的標記,用來告訴測試框架這個方法很特別;今天靠它指定哪些方法要在測試前後自動執行。
  • @RunWith(AndroidJUnit4):寫在測試 class 上的一行 Annotation,指定要用 Android 的 JUnit4 來跑這個測試,沒有它 Android Unit Test 不會動。
  • @Before:每次「任何一個」測試開始以前都會自動執行的方法;今天用來把變數、狀態準備成「最初」的樣子,例如取得 Context 和日記檔案。
  • @After:每次「任何一個」測試結束之後都會自動執行的方法;與 @Before 相反,用來清掃狀態,避免上一則測試影響下一則。
  • 測試 function:寫在測試 class 的 {} 裡頭的 Function,每個方法就是一則測試;今天三則測試分別檢查「兩個都空白要報錯」、「Title 空白也能存」、「Note 空白也能存」。
  • Android Context:在 AndroidTest 裡取得的 Android 執行環境資訊,有了它才能拿到 App 的檔案位置;因為要驗證 ViewModel「真的有建立日記檔案」,所以測試裡需要它。

上一篇
[Day16] 修正問題,沒有標準答案
下一篇
[Day18] 抽換更多東西,換得更好的測試
系列文
我的第一個手作りAndroid App!快樂學習物件導向程式設計!19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
饅頭
iT邦新手 5 級 ‧ 2026-09-05 10:43:13

小老鼠!!!

你是不是知道?那你的年齡...

1
chiaominchang222
iT邦新手 4 級 ‧ 2026-09-05 17:56:23

請問這邊文章有幾種語言了 也太精實的教程

到目前為止只有中文,英文

1
AndyAWD
iT邦新手 1 級 ‧ 2026-09-05 23:28:29

單元測試!要 TDD 了嗎

快到我的能力天花板了,每天都在想是不是該

我要留言

立即登入留言