LXC の Swift でユニットテスト
〜 Xcode を使わないコーディング 〜
2025-05-25 作成 福島
TOP > tips > swift-unittest
[ TIPS | TOYS | OTAKU | LINK | MOVIE | CGI | AvTitle | ConfuTerm | HIST | AnSt | Asob | Shell ]

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-
OSRocky Linux release 9.5 (Blue Onyx)本稿記述時の最新版
コンテナ環境LXC 4.0.12 Release 1.el9-
MacintoshmacOSSequoia 15.5本稿記述時の最新版
Xcode は macOS 用の IDE。他の OS では動作しない
Xcode16.3

Swift 言語のターゲットは主に MacintoshiPhone であり、Linux で動作しても SwiftUIXcode は使用できないけど、
フレームワークから独立したプログラムなら Linux でもソースコードの 作成文法チェック動作確認 ができます。
(プログラムはフレームワークに対して疎結合で構成したほうが開発効率・信頼性・応用性が上がり、ユニットテストはそのために存在する)

注意:Swift は LLVM コンパイラだけど、生成されるバイナリは OS や CPU に依存するので、ユニットテストに合格してもソースコード互換です。


1. テスト環境を確認・変更 (macOS で実施。Linux ではやらない)

Testing が macOS で正常に動作するようにする。
(GUI の Xcode は影響を受けない)

1-1. テスト環境を確認する。
% xcode-select -p
/Library/Developer/CommandLineTools         

1-2. テスト環境を変更する。
% sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
% xcode-select -p
/Applications/Xcode.app/Contents/Developer  


2. テスト環境の作成と確認

最初は動作確認のため、自前のコードを記述せずデフォルトの環境のまま実行する。
自前のコードは、下記 34 でテストする。

2-1. テスト環境を作成する。
$ mkdir swift-test/
$ 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
2-2. テスト対象のプログラムファイルを確認する。
swift-test$ cat ./Sources/lib1/lib1.swift

プログラム本体。まだ自動生成されたコメントだけ。
// The Swift Programming Language
// https://docs.swift.org/swift-book  

自分のプログラムはこのファイルに記述する。

既存のプログラムファイルを追加する方法は、下記 4 を参照。
(下記 4 ではプログラムを新規作成しているが、追加方法は同じ)
2-3. テストプログラムを確認する。
swift-test$ cat ./Tests/lib1Tests/lib1Tests.swift

ここにチェック用プログラムを追記していく。
import Testing
@testable import lib1

@Test func example() async throws {
    // Write your test here and use APIs like `#expect(...)` to check expected conditions.*1  
}
*1「#expect() 等の API を使って、求める条件を記述しろ」と書いてある。
2-4. デフォルトのまま実行する。
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.swift

例: カウンターのプログラム - 内部の値を add() の引数で加算する。
// 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 } }
*2他のモジュールを必要とする場合は、import を記述する。(Xcode で不要でも swift test では必要)
3-2. テストするプログラムを記述する。
swift-test$ vim ./Tests/lib1Tests/lib1Tests.swift

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 になっているはず }
3-3. テストを実行する。
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. プログラムを追加する。
モジュールはディレクトリのことで、その中に一つ以上のプログラムファイルを設置する。
(「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
4-2. テストするプログラムを変更する。
swift-test$ vim ./Tests/lib1Tests/lib1Tests.swift

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 になっているはず }
*3Swift のユニットテストではテスト関数が同時実行され実行順が不定になるため、
表示が必要な場合は関数名を併記する。(#function は実行中の関数名が入るマクロ)
4-3. パッケージ情報を変更する。
コマンドは対応していない。手動で変更する。
追加するモジュール名 (ディレクトリ名) をすべて記述する。

swift-test$ vim ./Package.swift

// 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"]  // <- ここに追加
        ),
    ]
)
*4もし他モジュールを必要とするなら括弧の中に ,dependencies:["他モジュール名"] も追記する。
 その場合は .target(name: "他モジュール名"), の記述も必要。
4-4. テストを実行する。
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 にも記述したが、以下のように使用する。動作はしない。
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)  
*5設置方法はこの README.md ファイルを参照。
Firefox なら Markdown Viewer Webext で読むと良い。