swift test で TDD をはじめる — FizzBuzz で Red/Green/Refactor を回す
CircleCI で iOS ビルドのパイプラインを調べる必要が出て、Swift を触りはじめました。普段は TypeScript が中心なので、まずテスト環境がどうなっているか確認することにしました。swift pac […]
目次
CircleCI で iOS ビルドのパイプラインを調べる必要が出て、Swift を触りはじめました。普段は TypeScript が中心なので、まずテスト環境がどうなっているか確認することにしました。swift package init を叩くと、テスト環境がすでに整っていました。手順をメモしておきます。
swift package init で何が生まれるか
ターミナルでパッケージを作成します。
mkdir first-swift-app
cd first-swift-app
swift package init --type executable
以下のファイルが自動生成されます。
.gitignore
Package.swift
Sources/first-swift-app/first_swift_app.swift
Tests/first-swift-appTests/first_swift_appTests.swift
swift run を実行すると、Hello, world! が出力されます。
swift run
# Hello, world!
次に swift test を打ちます。
swift test
最初から1テストが通ります(Green)。生成されたテストファイルを見ると import Testing と書かれており、Swift Testing が使われています。これは Swift 6 / Xcode 16 以降の標準スタイルです。環境によっては import XCTest の従来スタイルが生成される場合もあります。
FizzBuzz で TDD サイクルを1回回す
TDD の流れを確認するために、FizzBuzz 関数を作ります。
Red: テストを書いて落とす
Sources/first-swift-app/first_swift_app.swift を以下に書き換えます。関数の中身は空にして、意図的にテストを落とします。
func fizzBuzz(_ number: Int) -> String {
return ""
}
Tests/first-swift-appTests/first_swift_appTests.swift にテストを書きます。
import Testing
@testable import first_swift_app
@Test func testFizzBuzz() {
#expect(fizzBuzz(3) == "Fizz")
}
swift test を実行すると、狙い通り失敗します。
✘ Test testFizzBuzz() recorded an issue at first_swift_appTests.swift:6:5: Expectation failed: (fizzBuzz(3) → "") == "Fizz"
✘ Test testFizzBuzz() failed after 0.001 seconds with 1 issue.
エラーメッセージが (fizzBuzz(3) → "") == "Fizz" と、実際の返り値まで表示されるので原因がすぐわかります。
Green: 最短で通す
まずテストを通すだけのコードを書きます。
func fizzBuzz(_ number: Int) -> String {
if number == 3 {
return "Fizz"
}
return ""
}
swift test を実行すると通ります。
✔ Test testFizzBuzz() passed after 0.001 seconds.
Refactor: テストを増やして本実装にする
5の倍数と15の倍数のテストを追加して、一度落とします。
@Test func testFizzBuzz() {
#expect(fizzBuzz(3) == "Fizz")
#expect(fizzBuzz(5) == "Buzz")
#expect(fizzBuzz(15) == "FizzBuzz")
#expect(fizzBuzz(1) == "1")
}
本実装に書き換えます。15の倍数を先に判定するのは、3と5の両方の倍数になるためです。
func fizzBuzz(_ number: Int) -> String {
if number % 15 == 0 { return "FizzBuzz" }
if number % 3 == 0 { return "Fizz" }
if number % 5 == 0 { return "Buzz" }
return String(number)
}
swift test を実行して全テストが通れば完了です。
TypeScript との対比メモ
TypeScript に慣れていると、Swift の記法で引っかかる部分がいくつかあります。
@Test と #expect() は、Vitest の test() と expect() に対応します。テスト関数の前に @Test をつけるだけで認識されるので、クラスを継承する必要がありません。
_ を引数名の前につけると、呼び出し側でラベルを省略できます。fizzBuzz(_ number: Int) と書くことで、fizzBuzz(3) のように呼べます。書かない場合は fizzBuzz(number: 3) という形になります。
String(number) は TypeScript の String(number) と同じ書き方です。
コマンドラインだけで swift test が動き、Red/Green/Refactor のサイクルを回せる環境が最初から整っています。TypeScript + Vitest に慣れていれば、違和感は少ないと思います。
まとめ
swift package init --type executable → swift run → swift test の3コマンドで、テストが動く環境が整います。あとは FizzBuzz を題材に Red → Green → Refactor を1サイクル回せば、流れは掴めます。
次は CircleCI での iOS ビルドパイプラインの設定に戻る予定です。