クラウド Mac で iOS の CSV 書き出しを検証する
チームが iOS アプリの記録を CSV に書き出し、リモートのビルドノードでテストした。単体テストでは「ファイルの書き込みに成功」と出たのに、同僚がファイルを開くと、メモが2行に分かれ、別のユーザー入力は表計算ソフトの数式になっていた。ここで検証すべきなのは共有ボタンではなく、書き出されたバイト列、フィールドの境界、危険な入力の扱いだ。クラウド Mac を使えば、手作業を要しないこれらのチェックを毎回のビルドに組み込める。
まず書き出しの仕様を明確にする
この記事では、テキストの仕様を次のように定める。出力は UTF-8 で、バイトオーダーマークは付けない。すべてのフィールドを二重引用符で囲み、フィールド内の二重引用符は2つ重ねる。レコード間とファイル末尾には CRLF を使う。フィールド内の改行はそのまま残すため、CSV 全体を改行文字で分割してレコード数を数えてはいけない。
安全上のルールも設ける。自由入力フィールドの前後から空白と制御文字を除いて確認し、先頭が =、+、-、@ のいずれかなら書き出しをエラーにする。このルールでは、通常のテキストとして扱える負数も一部拒否される。数値列が必要な製品なら、すべてのテキスト列の制限を緩めるのではなく、列の定義に従って数値を解析・検証する。ヘッダーにも同じエンコード関数を使い、エスケープ処理が二重に実装されるのを防ぐ。
CSV の引用符が解決するのはフィールドの区切りであり、表計算ソフトによる数式の解釈ではない。テキストの先頭に単一引用符を付けても、インポートツールによって結果が同じとは限らない。危険な入力を拒否するほうが仕様とテストを定めやすいが、対象のインポートツールでの動作確認は必要だ。
検証可能なバイト列を1つの関数で生成する
エンコード処理は、アプリとテストターゲットの両方から参照できる 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 が空でないことだけでなく、実際の出力を比較する。最初のテストはカンマ、引用符、中国語、フィールド内の改行を確認する。2つ目は、先頭のタブ文字で検査を回避できないことを確認する。
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 のエンコード処理だけで、GUI には依存しない。共有フローも検証する場合は、UI テスト用のセッションと権限設定を別途用意する。
テストデータから実際のファイルまで確認する
単体テストが通ったら、アプリの実際の書き出し経路で固定のサンプルファイルを生成する。サンプルには少なくとも空フィールド、カンマ、二重引用符、フィールド内の改行、中国語を含める。共有処理の側で文字列を再連結していないか、ファイルが同じ csvDocument から生成されたかを確認する。アプリが書き込める場所に保存する際は、Data.write(to:options: .atomic) を使うと書き込み途中のファイルを読み取る事態を避けられる。共有に失敗したあとの一時ファイルの回収も、アプリのフローに沿ってテストする。
ターミナルで「行数」だけを見て成功と判断してはいけない。引用符内にも改行があり得る。サンプルと照合しながらフィールドごとにインポートし、列の順序、最終行、エラー表示を確認するほうが確実だ。インポートツールが識別番号を勝手に数値へ変えたり、日付に見えるテキストを日付へ変えたりするなら、それは別の形式上の要件となる。明示的な列型や専用の交換形式で対処すべきであり、CSV の引用符でセルの型まで固定できると考えてはいけない。
マージ前のチェックリスト
- ヘッダーとデータ行で列数と順序が一致し、空値が仕様どおりに書き出されている。
- UTF-8 テキスト、フィールド内の引用符・カンマ・改行を正確な値でアサートしている。
- 数式と解釈され得る入力ではユーザーに見えるエラーが発生し、未検証の代替ファイルを生成しない。
- CI ではノード上に実在するシミュレータを使用し、実行ごとに独立したテスト結果を残す。
- 共有処理はテスト済みのバイト列を読み取り、対象のインポートツールでの手動検証では型の自動変換があれば記録している。
CSV の書き出しで最も見落としやすい問題は、正常なサンプルではなく、実際のユーザーが入力した一件のメモに潜んでいることが多い。境界条件を先に固定のテストにし、最後に実ファイルを確認するほうが、ファイル生成後に目視で不具合を探すよりはるかに確実だ。
よくある質問
フィールド全体を引用符で囲めば、内部の引用符はそのままでよいですか?
いいえ。フィールド内の二重引用符を二つに増やしてから、フィールド全体を二重引用符で囲みます。
先頭にアポストロフィを付ければ、どの表計算ソフトでも安全ですか?
保証できません。読み込み時の解釈は製品ごとに異なるため、例では疑わしい値を拒否し、利用する読み込み先でも別途検証します。
次のXcodeビルドを専用物理マシンで実行
Apple Siliconのモデル、拠点、利用期間を選び、リモート接続して独自のビルド環境を実行できます。実際の提供条件は、ご注文時にコンソールでご確認ください。