團隊把 iOS App 中的紀錄匯出成 CSV,放到遠端建置節點執行測試。單元測試顯示「檔案寫入成功」,同事開啟檔案後卻發現備註被拆成兩行,另一筆使用者輸入則變成試算表公式。這裡要驗收的不是分享按鈕,而是匯出的位元組、欄位邊界,以及危險輸入的處理結果。雲端 Mac 適合把這些不需人工點選的檢查固定在每次建置流程中。
先訂清楚匯出規格
本文採用明確的文字規格:輸出 UTF-8、不含位元組順序標記;每個欄位都以雙引號包住,欄位內的雙引號則寫成兩個;紀錄之間與檔案結尾都使用 CRLF。欄位內的換行保留原樣,不能先按換行符號拆開整份 CSV 再計算紀錄數。
另訂一條安全規則:自由輸入欄位去除前後空白及控制字元後,若以 =、+、- 或 @ 開頭,就讓匯出失敗。這也會拒絕某些原本可視為一般文字的負數;若產品確實需要數值欄,應先依欄位定義解析並驗證數值,而不是放寬所有文字欄位。表頭也使用同一個編碼函式,避免維護兩套跳脫邏輯。
CSV 的引號處理的是欄位分隔,不是試算表對公式的解讀。在文字前加上單引號,不同匯入工具未必會得到相同結果;拒絕危險輸入較容易定義與測試,但仍須核對目標匯入工具的行為。
用單一函式產生可檢查的位元組
將編碼邏輯放在 App 與測試目標都能引用的 Swift 檔案中。以下函式接收二維字串陣列;實際應用可先把業務模型轉成指定順序的欄位。trimmingCharacters 只用於偵測風險,輸出時仍保留原值,不會悄悄修改使用者資料。
import Foundation
enum CSVExportError: Error {
case unsafeValue
}
func csvField(_ value: String) throws -> String {
let ignored = CharacterSet.whitespacesAndNewlines
.union(.controlCharacters)
let probe = value.trimmingCharacters(in: ignored)
if let first = probe.first, "=+-@".contains(first) {
throw CSVExportError.unsafeValue
}
let escaped = value.replacingOccurrences(of: "\"", with: "\"\"")
return "\"" + escaped + "\""
}
func csvDocument(_ rows: [[String]]) throws -> Data {
let lines = try rows.map { row in
try row.map(csvField).joined(separator: ",")
}
return Data((lines.joined(separator: "\r\n") + "\r\n").utf8)
}
呼叫端還應檢查每列的欄位數是否符合表頭、決定空資料集是只匯出表頭還是提示使用者,並將錯誤轉成可見的提示。不要捕捉 unsafeValue 後改為輸出未經檢查的原文;那會讓測試通過,實際分享的檔案卻繞過規則。若匯出內容包含敏感資料,也應在分享前確認目標檔案及其存取權限。
同時測試正常路徑與拒絕路徑
測試應比較實際輸出,而不只是斷言 Data 非空。第一個範例涵蓋逗號、引號、中文及欄位內換行;第二個範例確認前置定位字元無法繞過檢查。
import XCTest
final class CSVExportTests: XCTestCase {
func testQuotingAndLineEndings() throws {
let data = try csvDocument([
["name", "note"],
["张三", "甲,\"乙\"\n丙"]
])
XCTAssertEqual(
String(decoding: data, as: UTF8.self),
"\"name\",\"note\"\r\n\"张三\",\"甲,\"\"乙\"\"\n丙\"\r\n"
)
}
func testFormulaLikeTextIsRejected() {
for value in ["=1+1", "\t@SUM(A1:A2)", "+1", " -2"] {
XCTAssertThrowsError(try csvDocument([[value]]))
}
}
}
這組測試證明輸出符合約定格式,也驗證了拒絕策略;但它不能證明每種試算表都會如預期匯入。尤其是引號內的換行、外觀像日期的文字,以及非 ASCII 字元,仍須用實際交付對象採用的匯入方式驗收。
在建置節點留下可複查的結果
先在雲端 Mac 確認 Xcode 命令列工具可用,再使用專案中已共享的 scheme 執行測試。將範例名稱換成儲存庫中的實際名稱;執行前可用 xcodebuild -list -project App.xcodeproj 核對入口。測試目標應能直接存取編碼檔案,不要為測試另複製一份實作。
xcodebuild test \
-project App.xcodeproj \
-scheme App \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:AppTests/CSVExportTests \
-resultBundlePath "$PWD/CSVExport.xcresult"
模擬器名稱須與該節點已安裝的執行階段相符,可先執行 xcrun simctl list devices available 檢查。執行前,CSVExport.xcresult 指定的路徑不得已存在;重複執行時,應為每次作業指定獨立目錄,避免舊結果干擾。這項測試只驗證純 Swift 編碼,不依賴圖形介面。若還要驗收分享流程,應另行為 UI 測試準備工作階段與權限條件。
從測試資料檢查到實際檔案
單元測試通過後,透過 App 實際的匯出路徑產生一份固定樣本,至少包含空欄位、逗號、雙引號、欄位內換行及中文。確認檔案確實由同一個 csvDocument 產生,而不是由分享層再次拼接字串。儲存到 App 可寫入的位置時,可使用 Data.write(to:options: .atomic),避免讀到寫入一半的檔案;分享失敗後如何清理暫存檔,也應依 App 流程測試。
不要只靠終端機顯示的「行數」判斷是否成功:引號內原本就可能有換行。更穩妥的做法是依樣本逐欄匯入比對,並檢查欄位順序、最後一列及錯誤提示。若匯入工具自行把編號轉成數字,或把日期文字轉成日期,那是另一項格式規格,需要透過明確的欄位型別或專用交換格式解決,不能假設 CSV 引號能固定儲存格型別。
合併前的檢查清單
- 表頭與資料列的欄位數及順序一致;空值依規格寫出。
- 對 UTF-8 文字、欄位內引號、逗號及換行都有精確斷言。
- 公式型輸入會觸發可見錯誤,不會產生未經檢查的替代檔案。
- CI 使用節點上實際存在的模擬器,並為每次執行保留獨立的測試結果。
- 分享層讀取的是已測試的位元組;在目標匯入工具上進行的人工驗收,記錄了任何自動型別轉換。
CSV 匯出最難察覺的問題,往往不在一般範例,而在某筆真實使用者的備註。先把邊界輸入變成固定測試,再檢查最終檔案,遠比檔案產生後才靠肉眼排錯可靠。
常見問題
欄位已用雙引號包住,內部引號還需要跳脫嗎?
需要。將欄位內每個雙引號寫成兩個雙引號,再以雙引號包住整個欄位。
在欄位前加單引號,能保證試算表不執行公式嗎?
不能。匯入器的處理方式不同;本文採用拒絕危險文字的策略,正式流程仍須以目標匯入器測試。
將下一次 Xcode 建置作業移至獨享實體主機。
選擇 Apple Silicon 機型、節點與租期,透過遠端連線執行自己的建置流程。實際可用情況以訂購時控制台顯示為準。