7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift TestingとXCTestを使い比べてみました

Posted at

はじめに

Swift Testing とは
Swift Testingは、Swift 6.0およびXcode 16.0から導入されたテスティングフレームワークであり、Swift言語本来の表現力を活かし、「より少ないコード」で直感的かつ強力なテスト(並行処理対応やパラメタライズテストなど)を記述できるのが最大の特徴となっています。

なぜこの比較を行ったのか
Swift Testingについては以前から興味を持っていたものの、なかなか触れる機会がなくそのままになっていました。
今回記事を書く機会ができたので、せっかくなら「実際のプロダクトコードではどう違うのか?」を確かめてみようと思い、実際のアプリケーション構成で各層のテストを書き比べることにしました。

今回はその全体像をざっと把握する目的でSwift Testingを触ってみて、普段使っているXCTestと何がどう違うのかを軽く比較してみます。

検証環境: Xcode 26.2

この記事で分かること

  • Swift TestingとXCTestの実際の書き方の違い - 同じテストケースを両方で実装した具体的なコード比較
  • Swift Testing導入時に実感できるメリット - パラメタライズテスト、アサーション記法、エラーハンドリングの変更点
  • 実際のプロダクト開発での適用イメージ - クリーンアーキテクチャ構成における実践的な活用例

今回の検証で使用するアプリ

Swift TestingとXCTestを比較するために、シンプルな構成のQiita記事閲覧アプリを作成しました。実際のプロダクトを想定しつつも、各レイヤーの責務が分かりやすい題材として今回の構成を選んでいます。

今回はこちらのアプリを題材に、Swift TestingとXCTestをそれぞれ使ってテストを書き比べていきます。

アーキテクチャ構成

今回のアプリでは、以下のようなシンプルなクリーンアーキテクチャ構成を採用しています。

  • Domain層: 記事を表すモデル (QiitaArticle)
  • Application層: ビジネスロジック (ArticlesUseCase)
  • Presentation層: SwiftUIを用いて構成した画面,画面の状態管理(ArticlesView, ArticlesViewModel
  • Infrastructure層: データ永続化とAPI通信(ArticlesDataSource

これらの各層で同じテストケースを両フレームワークで実装し、実際の書き方や特徴を今回比較してみました。

Domain層 - Modelのテスト

🎯 アサーション記法の違いとテスト宣言の方法

まずは Domain層におけるモデルのテストから見ていきます。
ここでは記事データを表すモデルに対して、純粋なロジックや値の振る舞いが正しく動作するかを確認するテストを抜粋し、比較してみます。

XCTest

func testArticleInitialization() {
    let article = QiitaArticle(id: "test", title: "記事")
    // Assert
    XCTAssertEqual(article.id, "test")
    XCTAssertFalse(article.isFavorite)
}

Swift Testing

@Test("Article initialization with required parameters")
func articleInitialization() async throws {
    let article = QiitaArticle(id: "test", title: "記事")
    // Assert
    #expect(article.id == "test")
    #expect(article.isFavorite == false)
}

Application層 - UseCaseのテスト

🎯 複数テストケースの簡潔な表現

次に Application層のUseCaseに対するテストを見ていきます。
ここではページネーション処理を題材に、無効な入力値に対する振る舞いをXCTestとSwift Testingでそれぞれどのように表現できるかを比較します。

XCTest

  func testInvalidPaginationParametersPage0() async throws {
      do {
          // Act
          try await useCase.execute(page: 0, perPage: 10)
          XCTFail("Expected ApplicationError but operation succeeded")
      } catch is ApplicationError {
          // Expected error
      } catch {
          XCTFail("Unexpected error type: \(error)")
      }
      // Assert
      XCTAssertEqual(mockArticlesRepository.getArticlesCallCount, 0)
  }

  func testInvalidPaginationParametersNegativePage() async throws {
      do {
          // Act
          try await useCase.execute(page: -1, perPage: 10)
          XCTFail("Expected ApplicationError but operation succeeded")
      } catch is ApplicationError {
          // Expected error
      } catch {
          XCTFail("Unexpected error type: \(error)")
      }
      // Assert
      XCTAssertEqual(mockArticlesRepository.getArticlesCallCount, 0)
  }
:
:(他のケースは省略)

Swift Testing

  @Test("Invalid pagination parameters should be rejected", arguments: [
      (0, 10), // page 0は無効
      (-1, 10), // 負のページ番号
      (1, 0), // perPage 0は無効
      (1, -5), // 負のperPage
      (1, 101) // perPageの上限超過(最大100に今回設定してます)
  ])
  func shouldRejectInvalidPaginationParameters(page: Int, perPage: Int) async throws {
      // Act&Assert
      await #expect(throws: ApplicationError.self) {
          try await useCase.execute(page: page, perPage: perPage)
      }
  }

Presentation層 - ViewModelのテスト

🎯 複数状態の一括検証における可読性

続いてPresentation層のViewModelに対するテストを見ていきます。
記事データの取得が成功した後の状態遷移を題材に、複数の値をどのように検証できるかを XCTest と Swift Testing で比較します。

XCTest

  func testLoadArticlesSuccess() async throws {
      XCTAssertTrue(viewModel.articles.isEmpty)
      XCTAssertFalse(viewModel.isLoading)
      XCTAssertNil(viewModel.error)

      // Act
      await viewModel.loadArticles()

      // Assert
      XCTAssertEqual(viewModel.articles.count, 3)
      XCTAssertEqual(viewModel.articles[0].title, "Swift Testing入門")
      XCTAssertEqual(viewModel.articles[1].title, "Clean Architecture実践")
      XCTAssertEqual(viewModel.articles[2].title, "SwiftUI + SwiftData活用術")
      XCTAssertFalse(viewModel.isLoading)
      XCTAssertNil(viewModel.error)
      XCTAssertEqual(mockArticlesUseCase.executeCallCount, 1)
  }

Swift Testing

  @Test("Load articles - success")
  func loadArticlesSuccess() async throws {
      #expect(viewModel.articles.isEmpty)
      #expect(viewModel.isLoading == false)
      #expect(viewModel.error == nil)

      // Act
      await viewModel.loadArticles()

      // Assert
      #expect(viewModel.articles.count == 3)
      #expect(viewModel.articles[0].title == "Swift Testing入門")
      #expect(viewModel.articles[1].title == "Clean Architecture実践")
      #expect(viewModel.articles[2].title == "SwiftUI + SwiftData活用術")
      #expect(viewModel.isLoading == false)
      #expect(viewModel.error == nil)
      #expect(mockArticlesUseCase.executeCallCount == 1)
  }

Infrastructure層 - DataSourceのテスト

🎯 エラーハンドリングテストの記述方法

最後にInfrastructure層のDataSourceに対するテストを見ていきます。
この層ではエラーハンドリングの書き方に注目しながらXCTestとSwift Testingの違いを確認していきます。

XCTest

  func testFetchArticlesJSONParsingError() async throws {
      // Arrange
      mockURLSession.mockData = Data("invalid json".utf8)
      mockURLSession.mockResponse = HTTPURLResponse(
          url: URL(string: "https://qiita.com/api/v2/items")!,
          statusCode: 200,
          httpVersion: nil,
          headerFields: ["Content-Type": "application/json"]
      )

      // Act & Assert
      do {
          try await dataSource.fetchArticles(page: 1, perPage: 10)
          XCTFail("Expected error but operation succeeded")
      } catch let error as InfrastructureError {
          // Expected error
          XCTAssertEqual(error, InfrastructureError.parsingError)
      } catch {
          XCTFail("Unexpected error type: \(error)")
      }

      XCTAssertEqual(mockAPIClient.requestCallCount, 1)
  }

Swift Testing

  @Test("Fetch articles - JSON parsing error handling")
  func fetchArticlesJSONParsingError() async throws {
      // Arrange
      mockURLSession.mockData = Data("invalid json".utf8)
      mockURLSession.mockResponse = HTTPURLResponse(
          url: URL(string: "https://qiita.com/api/v2/items")!,
          statusCode: 200,
          httpVersion: nil,
          headerFields: ["Content-Type": "application/json"]
      )

      // Act & Assert
      await #expect(throws: InfrastructureError.parsingError) {
          try await dataSource.fetchArticles(page: 1, perPage: 10)
      }

      #expect(mockAPIClient.requestCallCount == 1)
  }

実際に書いてみて感じた違い

テストの意図が構造として表現しやすい

Swift Testingでは@Testアトリビュートを記載することで、テストであることが宣言的に明示されます。
最初は新しい記法を覚える必要があることに抵抗感がありましたが、実際に使ってみると、XCTestのような命名規則に依存せず、コード上の構造としてテストを表現できる点が印象的でした。
特にDomain層やUseCase層のようにテスト対象の数が増えやすい箇所では、テストコード全体からテスト関数を探しやすくなっていると思いました。

アサーション表現が統一され、読みやすくなっている

XCTestでは、値の比較や状態の検証の際にXCTAssertEqualXCTAssertFalseなど複数種類のアサーションAPIを使い分ける必要があります。
一方Swift Testingでは、基本的に#expectで表現ができ、比較も==を用いた自然な形式で記述できます。
特にPresentation層のViewModelテストに挙げているような複数の型や状態をまとめて検証する場面では、アサーションの書き方にブレがなく、個人的にはテストコード全体が読みやすく感じました。

パターン別テストケースの記述がシンプルになる

Application層のUseCaseでの入力値のパターンによって結果が変わるテストに関して、XCTestでも工夫をすれば1つのテスト関数内に複数ケースをまとめることは可能かと思います。

一方でSwift Testingの引数付きテストでは、@Test(arguments:)を用いることで入力値をそのまま列挙でき、記述量をかなり減らせることが確認できました。XCTestで5つのテスト関数に分けていたものが、1つの関数で表現できるのは実用的だと感じています。
この点は、特にDomain層やApplication層のロジックテストと相性が良いのかなと思いました。

エラーハンドリングのテストが簡潔にかける

今回、Infrastructure層のDataSourceで挙げたようなエラーハンドリングの検証において、XCTestではdo-catchを用いてエラーを捕捉し、そこからアサーションを書く形式ですが、Swift Testingの#expectでは期待するエラー型を直接指定できるため、コードの意図がより明確になります。

慣れ親しんだXCTestの方が楽だろうと予想していましたが、実際に書き比べてみると、Swift Testingの方がテストの意図をコードとして素直に表現しやすい場面が多くありました。

まとめ

今回はシンプルな構成のアプリを題材に、実際にテストコードを書き比べることで、Swift TestingとXCTestの違いをざっと確認してみました。
Swift Testingには並列実行など、他にも今回触れきれなかった特徴も多くあるため、今後は業務で扱っているプロダクト等を通して、引き続き理解を深めていければと思います。

🔗 参考

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?