Swift Digest
SE-0019 | Swift Evolution

Swift Testing

Proposal
SE-0019
Authors
Max Howell, Daniel Dunbar, Mattt Thompson
Review Manager
Rick Ballard
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift Package Manager(SwiftPM)は、Swiftで書かれたライブラリや実行ファイルをパッケージ単位で配布・ビルドするための仕組みです。一方で、テストは現代的なソフトウェア開発に欠かせない要素であり、パッケージのエコシステムを安定して運用していくうえでもテストを手厚くサポートする必要があります。

しかし、この提案以前のSwiftPMにはテストを扱うための公式な仕組みが整っていませんでした。具体的には次のような状況でした。

パッケージのテストを走らせる標準の方法がない

パッケージの作者がテストを書こうとしても、テストターゲットをどこに置けばよいか、どう命名すればよいかといった規約がSwiftPMの慣習として定まっていませんでした。そのため、テストの配置やビルド方法がパッケージごとにまちまちになり、別のパッケージのテストを動かそうとしたときに手順を毎回確認しなければならない状態でした。

テストを起動するコマンドがない

swift build でビルドはできても、テストだけを選んで実行するような swift test 相当のコマンドがなく、CIへの組み込みや手元での繰り返し実行のハードルが高い状態でした。テスト結果のフォーマットについてもSwiftPM側の取り決めがなく、出力をツールから解釈するための共通の足場がありませんでした。

XCTestとの統合が自前任せ

Swiftのテスト基盤としてはXCTestが存在していたものの、SwiftPMからXCTestを扱うための標準的な経路がなく、Package.swift やディレクトリ構成の側で何を期待するのかが定まっていませんでした。結果として、テストモジュールがメインモジュールの internal な要素にアクセスするための testability(@testable import)の設定なども、パッケージ作者が個別に工夫する必要がありました。

テストは重要であるにもかかわらず導入の障壁が高いままでは、パッケージ全体の品質が上がっていきません。SwiftPMにテストを組み込み、できるだけ最小限の約束事で誰でもテストを書き始められるようにする必要がありました。

02 どのように解決されるのか

SwiftPMのディレクトリ規約を拡張してテストモジュールを一級市民として扱えるようにし、あわせて swift test コマンドを導入します。テストフレームワークとしてはまずXCTestを採用します。

Tests ディレクトリによる規約

パッケージルート直下の Tests ディレクトリ、または各モジュールディレクトリ内の Tests サブディレクトリを、テストモジュールとして扱います。たとえば次のような構成が認められます。

Package
├── Sources
│   └── Foo
│       └── Foo.swift
└── Tests
    └── Foo
        └── Test.swift
Package
└── Sources
    ├── Foo.swift
    └── Tests
        └── Test.swift
Package
├── Sources
│   └── Foo.swift
└── Tests
    └── TestFoo.swift

Tests/ の中に複数のサブディレクトリを置くと、それぞれが独立したテストモジュールになります。たとえば Tests/FooTests/Bar を用意すると2つのテストモジュールが生成され、どちらもモジュール Foo をテストしていて構いません。同じモジュールに対して観点ごとにテストを分割するのはパッケージ作者の裁量に任されます。

依存モジュールの自動判定

テストの導入コストを下げるため、テストターゲットの名前からテスト対象のモジュールを自動で推定します。たとえば名前が Foo のテストターゲットは、ライブラリターゲット Foo のビルドに依存するものとして扱われます。これ以外の依存関係や自動判定できない依存関係については、別途パッケージマニフェストで指定することが想定されています。

testabilityのデフォルト有効化

デバッグビルドではモジュールをtestability付きでビルドすることをデフォルトにします。これにより、テストモジュール側で @testable import を使って、対象モジュールの internal な要素にアクセスできます。利用者がこの設定を毎回指定しなくて済むようにするための判断です。

将来的には、モジュール自身がテスト用にビルドされていることをソースコード側から識別できるように、専用のdefineを提供することも検討されるとされています。

モジュールのビルドとテストのビルドを連動させる

あるモジュールをビルドすると、対応するテストモジュールも合わせてビルドされます。テストのビルド時間がわずかに増える代わりに、コード変更でテストが壊れたことをすぐに知ることができます。この挙動は意図的なもので、テストを常にビルド対象に含めることの価値を優先した設計です。

テストまで毎回実行するとデバッグサイクルを阻害するため、実行までは自動化しません。また、テストソースがコンパイルできない間などテストのビルドを一時的に止めたい場合のために、ビルドコマンド側でテストを除外するフラグを用意します。

$ swift build --without-tests

リリースビルドではtestabilityを有効化しない方針のため、リリースモードではテストのビルドも行いません。リリースモードでのテスト(パフォーマンステストなど)への対応は今後の課題とされています。

swift test コマンド

テストの実行には新しいサブコマンド swift test を導入します。

$ swift test

特定のテストケースや個々のテストだけを走らせることもできます。

$ swift test TestModule.FooTestCase
$ swift test TestModule.FooTestCase.test1

swift test に渡された認識できない引数は、下層のテストフレームワーク(XCTest)にそのまま転送され、フレームワーク側の解釈に委ねられます。

swift test は実行前に自動的にビルドを行います。テストは実行前にビルドが必要であること、そして一般的なテストツールが実行前にビルドを行うことを踏まえた、最も自然な挙動という判断です。ビルドを省きたい場合のためのフラグも用意されます。

出力の扱い

テスト実行時のターミナル出力は、成否を視覚的に確認しやすいよう色付けや整形を施した人間向けのフォーマットを基本とします。

$ swift test --output module
Running tests for PackageX (x/100)
.........x.....x...................

Completed
Elapsed time: 0.2s

98 Success
 2 Failure
 1 Warning

FAILURE: Tests/TestsA.swift:24 testFoo()
XCTAssertTrue expected true, got false

加えて、CIなどに組み込みやすいJUnit形式のXMLといった別フォーマットも、オプションで選べるようにします。

テスト専用の依存関係・ユーティリティコード

テスト専用の依存関係を指定する簡易な仕組みはすでに存在し、本提案もそれを前提としています。より高度な指定(テストからだけ利用したいユーティリティモジュールをパッケージ内に持つ仕組みなど)は、Package.swift の今後の拡張として別提案に委ねられます。

今後の方向性(Future Directions)

提案時点では初期実装に入らないものの、次のような拡張が想定されています。いずれもこの段階での約束ではなく、あくまで方向性として示されているものです。

  • テスト種別を絞って実行するためのオプション(例: swift test --kind=performance)。
  • ビルド対象を細かく指定できるようになった際に、特定のテストだけをビルドするオプション。
  • 依存パッケージのテストまで含めて走らせるためのフラグ。
  • デバッグ/リリースの構成をテスト側で別途指定できるようにする仕組み。
  • XCTest以外のテストフレームワークにも対応できるようにするため、SwiftPM側でプロトコルを定義してフレームワーク側が適合する方式。

利用者として知っておくべきこと

  • SwiftPMパッケージのテストは、パッケージルート直下の Tests/ 以下にテストモジュールを置き、swift test で実行するのが標準です。この提案で定められた規約は現在も基本として踏襲されています。
  • 初期実装のテストフレームワークはXCTestです。@testable import を用いた internal 要素へのアクセスは、デバッグビルドのtestabilityがデフォルトで有効になっているため追加設定なしで利用できます。
  • モジュールをビルドすると、対応するテストモジュールも一緒にビルドされます。テストのビルドだけを止めたいときは swift build --without-tests を使います。