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 executableswift runswift test の3コマンドで、テストが動く環境が整います。あとは FizzBuzz を題材に Red → Green → Refactor を1サイクル回せば、流れは掴めます。

    次は CircleCI での iOS ビルドパイプラインの設定に戻る予定です。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark