0. 前準備
Swift のユニットテストには Testing と XCTest の二種類があり、本稿では Testing (2024年9月の Swift 6 から) について記述しています。
ここでは LXC (Linux コンテナ) で Swift 言語を使用します。
LXC を使わない普通の Linux でも実施できます。
macOS では「ターミナル」で実施できます。
Xcode は基本的に XCTest 向きなので非対応です。(Xcode を Swift Package Manager に合わせれば動くけどかなり煩雑)
LXC 環境の構築はここ (1 ~ 5) を参考にしてください。
Swift 6 のインストールはここを参考にしてください。
macOS の Swift は AppStore から Xcode をインストールすると使える。
実行環境
項目 内容 備考 作業用ディレクトリ swift-test/ swift コマンドが自動的に .gitignore や .build/ を作るため、作業用ディレクトリを用意する チェック対象
(プログラム)./Source/lib1/lib1.swift クラス Counter を記述する ./Source/ModuleName/struct2.swift クラス Contra を記述する メインプログラム ./Tests/lib1Tests/lib1Tests.swift メインプログラム。この中からチェック対象を呼び出してチェックする プログラミング言語 Swift version 6.0.3
Target: x86_64-unknown-linux-gnu本稿記述時の最新版
Swift 言語自体はオープンソースコンテナ 名称 swift1 - OS Rocky Linux release 9.5 (Blue Onyx) 本稿記述時の最新版 コンテナ環境 LXC 4.0.12 Release 1.el9 - Macintosh macOS Sequoia 15.5 本稿記述時の最新版
Xcode は macOS 用の IDE。他の OS では動作しないXcode 16.3
Swift 言語のターゲットは主に Macintosh や iPhone であり、Linux で動作しても SwiftUI や Xcode は使用できないけど、
フレームワークから独立したプログラムなら Linux でもソースコードの 作成・文法チェック・動作確認 ができます。
(プログラムはフレームワークに対して疎結合で構成したほうが開発効率・信頼性・応用性が上がり、ユニットテストはそのために存在する)
注意:Swift は LLVM コンパイラだけど、生成されるバイナリは OS や CPU に依存するので、ユニットテストに合格してもソースコード互換です。
1. テスト環境を確認・変更 (macOS で実施。Linux ではやらない)
Testing が macOS で正常に動作するようにする。
(GUI の Xcode は影響を受けない)
1-1. テスト環境を確認する。
% xcode-select -p1-2. テスト環境を変更する。
/Library/Developer/CommandLineTools
% sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
% xcode-select -p
/Applications/Xcode.app/Contents/Developer
2. テスト環境の作成と確認
最初は動作確認のため、自前のコードを記述せずデフォルトの環境のまま実行する。
自前のコードは、下記 3 と 4 でテストする。
2-1. テスト環境を作成する。
$ mkdir swift-test/2-2. テスト対象のプログラムファイルを確認する。
$ cd swift-test/
swift-test$ swift package init --type library --name lib1
(単一で動作するプログラムを作成したい場合は --type executable を指定する → main の記述が必要)
ツリー構成を確認する。
swift-test$ LANG=c tree -a
. |-- .gitignore |-- Package.swift ← プログラムの構成ファイル (参照関係を指示する) |-- Sources | `-- lib1 | `-- lib1.swift ← チェック対象のプログラムファイル `-- Tests `-- lib1Tests `-- lib1Tests.swift ← チェック用プログラムファイル 4 directories, 4 files
swift-test$ cat ./Sources/lib1/lib1.swift2-3. テストプログラムを確認する。
プログラム本体。まだ自動生成されたコメントだけ。
// The Swift Programming Language // https://docs.swift.org/swift-book
自分のプログラムはこのファイルに記述する。
既存のプログラムファイルを追加する方法は、下記 4 を参照。
(下記 4 ではプログラムを新規作成しているが、追加方法は同じ)
swift-test$ cat ./Tests/lib1Tests/lib1Tests.swift2-4. デフォルトのまま実行する。
ここにチェック用プログラムを追記していく。
*1「#expect() 等の API を使って、求める条件を記述しろ」と書いてある。
import Testing @testable import lib1 @Test func example() async throws { // Write your test here and use APIs like `#expect(...)` to check expected conditions.*1 }
swift-test$ swift test
テストはまだ example() だけだが動作する。
Building for debugging... [26/26] Linking lib1PackageTests.xctest Build complete! (10.24s) Test Suite 'All tests' started at 2025-05-09 14:57:51.877 Test Suite 'debug.xctest' started at 2025-05-09 14:57:51.880 Test Suite 'debug.xctest' passed at 2025-05-09 14:57:51.880 Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds Test Suite 'All tests' passed at 2025-05-09 14:57:51.880 Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds ◇ Test run started. ↳ Testing Library Version: 6.0.3 ◇ Test example() started. ✔ Test example() passed after 0.001 seconds. ✔ Test run with 1 test passed after 0.001 seconds. }
3. プログラムを作成してテスト
3-1. テストされるプログラムを記述する。
swift-test$ vim ./Sources/lib1/lib1.swift3-2. テストするプログラムを記述する。
例: カウンターのプログラム - 内部の値を add() の引数で加算する。
*2他のモジュールを必要とする場合は、import を記述する。(Xcode で不要でも swift test では必要)
// The Swift Programming Language // https://docs.swift.org/swift-book
// import OtherModule*2 public // for TEST - テスト時は (Xcode で不要でも) public を記述する struct Counter { var number:Int = 0 // 最初はゼロ public mutating func add(_ n: Int) -> Int { number += n return number } }
swift-test$ vim ./Tests/lib1Tests/lib1Tests.swift3-3. テストを実行する。
import Testing @testable import lib1 /* 初期化時に生成された example() は使用しない@Test func example() async throws { // Write your test here and use APIs like `#expect(...)` to check expected conditions.}*/
@Test func test_001() { // テスト名称は内容と関係なくても良い // カウンター (テスト対象) を用意する var cnt:Counter = Counter() // 最初はゼロ var ans:Int ans = cnt.add(2) // カウンターに 2 を加算してみる #expect(2 == ans) // 2 になっているはず ans = cnt.add(-3) // カウンターから 3 を減算してみる #expect(-1 == ans) // -1 になっているはず }
swift-test$ swift package clean # ← 前回のキャッシュをクリアする (省略可) … rm -Rf .build/ なら、さらに強力。
swift-test$ swift test
[26/26] Linking lib1PackageTests.xctest Build complete! (10.51s) Test Suite 'All tests' started at 2025-05-09 16:10:30.625 Test Suite 'debug.xctest' started at 2025-05-09 16:10:30.626 Test Suite 'debug.xctest' passed at 2025-05-09 16:10:30.626 Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds Test Suite 'All tests' passed at 2025-05-09 16:10:30.626 Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds ◇ Test run started. ↳ Testing Library Version: 6.0.3 ◇ Test test_001() started. ✔ Test test_001() passed after 0.001 seconds. ✔ Test run with 1 test passed after 0.001 seconds.
4. テスト対象の追加
4-1. プログラムを追加する。
モジュールはディレクトリのことで、その中に一つ以上のプログラムファイルを設置する。4-2. テストするプログラムを変更する。
(「import ModuleName」はディレクトリ名が ModuleName であることを指している)
swift-test$ mkdir ./Sources/ModuleName/
swift-test$ vim ./Sources/ModuleName/struct2.swift
例: カウンターのプログラム - 内部の値を add() の引数で減算する。(あまのじゃく版)
public // for TEST - テスト時は (Xcode で不要でも) public を記述する struct Contra { var number:Int = 0 // 最初はゼロ public mutating func add(_ n: Int) -> Int { number -= n return number } }
ツリー構成を確認する。
swift-test$ LANG=c tree -a
. |-- .build ← 上記 3 によって自動生成されたワーク | |-- artifacts | |-- .gitignore |-- Package.swift |-- Sources | |-- ModuleName | | `-- struct2.swift ← このファイルを作成した | `-- lib1 | `-- lib1.swift `-- Tests `-- lib1Tests `-- lib1Tests.swift 357 directories, 494 files
swift-test$ vim ./Tests/lib1Tests/lib1Tests.swift4-3. パッケージ情報を変更する。
*3Swift のユニットテストではテスト関数が同時実行され実行順が不定になるため、
import Testing @testable import lib1 /* 初期化時の example() は使用しない@Test func example() async throws { // Write your test here and use APIs like `#expect(...)` to check expected conditions.}*/ @Test func test_001() { // テスト名称は内容と関係なくても良い // カウンター (テスト対象) を用意する var cnt:Counter = Counter() // 最初はゼロ var ans = cnt.add(2) // カウンターに 2 を加算してみる #expect(2 == ans) // 2 になっているはず ans = cnt.add(-3) // カウンターから 3 を減算してみる #expect(-1 == ans) // -1 になっているはず }
@testable import ModuleName @Test func 加算と見せかけて減算() { // 日本語でも OK print("-- \(#function) : message") // <- 表示が必要な場合*3 // あまのじゃくカウンター (テスト対象) を用意する var cnt:Contra = Contra() // 最初はゼロ var ans:Int ans = cnt.add(2) // カウンターから 2 を減算してみる #expect(-2 == ans) // -2 になっているはず ans = cnt.add(-3) // カウンターに 3 を加算してみる #expect(1 == ans) // 1 になっているはず }
表示が必要な場合は関数名を併記する。(#function は実行中の関数名が入るマクロ)
コマンドは対応していない。手動で変更する。4-4. テストを実行する。
追加するモジュール名 (ディレクトリ名) をすべて記述する。
swift-test$ vim ./Package.swift
*4もし他モジュールを必要とするなら括弧の中に ,dependencies:["他モジュール名"] も追記する。
// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "lib1", products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "lib1", targets: ["lib1"]), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "lib1"), .target(name: "ModuleName"), // <- ここに追加*4 .testTarget( name: "lib1Tests", dependencies: ["lib1","ModuleName"] // <- ここに追加 ), ] )
その場合は .target(name: "他モジュール名"), の記述も必要。
swift-test$ swift package clean # ← 前回のキャッシュをクリアする (省略可) … rm -Rf .build/ なら、さらに強力。
swift-test$ swift test
Building for debugging... [31/31] Linking lib1PackageTests.xctest Build complete! (10.97s) Test Suite 'All tests' started at 2025-05-09 17:44:10.481 Test Suite 'debug.xctest' started at 2025-05-09 17:44:10.482 Test Suite 'debug.xctest' passed at 2025-05-09 17:44:10.482 Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds Test Suite 'All tests' passed at 2025-05-09 17:44:10.482 Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds ◇ Test run started. ↳ Testing Library Version: 6.0.3 ◇ Test 加算と見せかけて減算() started. ← 追加したテストの分 ◇ Test test_001() started. -- 加算と見せかけて減算() : message ← 追加したテストからの表示 ✔ Test 加算と見せかけて減算() passed after 0.001 seconds. ← 追加したテストの分 ✔ Test test_001() passed after 0.001 seconds. ✔ Test run with 2 tests passed after 0.001 seconds.
5. おまけ
DummyUI の作成について
SwiftUI を使えないのは分かっているが、アルゴリズムの確認ぐらいはしたい。
例えば、複雑な図形を描画する場合、その経路情報は数値の集合であり、
実際に描画するのは数値の集合を作成してからとなるが、集合を表すクラスは SwiftUI の「Path」を使用する。
このときユニットテストで確認したいのは Path そのものではなく数値の集合で、ソースコードはなるべく実装に近づけたい。
このような必要性により SwiftUI は使えないまでも、それに近づけるためのモジュール DummyUI を作成した。
ダウンロードは ここ (zip ファイル) から。
当然だが Xcode の環境に入れてはいけない。
使用方法:
zip ファイルに含まれる README.md*5 にも記述したが、以下のように使用する。動作はしない。
*5設置方法はこの README.md ファイルを参照。
var path:Path = Path() path.move(to:CGPoint(x:10,y:10)) path.closeSubpath() let gc:GraphicsContext = GraphicsContext() gc.stroke(path, with: .color(Color.black), lineWidth:1)
Firefox なら Markdown Viewer Webext で読むと良い。