Swift Playgrounds のアプリでテストを書く


# 初めに

まず最初に **Xcode の Playground とは違います** のでmm
今回は **Swift Playgrounds** でアプリを開発する場合のお話です。



## Swift Playgrounds とは

2016年に登場した iPad 上で Swift を学習できるアプリです。

https://www.apple.com/jp/swift/playgrounds/


当初はテンプレートの教材を Swift で学習するだけでした。

現在はバージョンが`4.1`までリリースされており、iPad のみならず Mac 上でも動作し、**実際にリリースできるアプリが作成できる**までになりました。

## どんなことができるの？

iOSDC Japan 2022 で LT をしました！
資料をあげたので、見ていただけると少しは理解が深まるかと思いますmm

https://speakerdeck.com/hcrane/iosdc2022-ipadosdc-japan-2022


# テストコードの実装方法

今回は一部の作業を iPad ではなく Mac で行う必要があります。
iPad でもファイル操作可能なアプリを入れればできるかと思いますが、未確認です。

## 1. 前提

`Swift Playgrounds` では、テストを書く機構がありません。
また、Xcode でアプリを開いたとしても、追加することはできません。

そこでライブラリを使っていきます。

https://github.com/Losiowaty/PlaygroundTester

`Swift Playgrounds` で作成するアプリで、テストをかけるようにするライブラリです。

ただし、`Swift Playgrounds` は、そもそも複数のターゲットを持てません。
つまり、ライブラリを追加しても、**プロダクションコードの中にテストを書く**ことになります。

ライブラリのコードは、ビルドして配布する過程で除かれるので、容量にはほとんど影響がありません。

## 補足

ライブラリのテスト実行方法ですが、以下の記事にも言及されている方法なのではないかと思います
（ざっとライブラリコードを見た感じですが）

https://www.swiftbysundell.com/articles/review-swift-playgrounds-30-for-ipad/

内部的には `Objective-C` のランタイムを使って、テストの実行を行なっていそうです。

## 2.導入

### ① Swift Package Manager での導入

`Swift Playgrounds` のデフォルトで唯一サポートされている `Swift Package Manager` で追加します。


### ② コンパイル時の設定を追加

① だけでも動作しますが、フラグを追加しないとアプリ配布時に**テストコードが含まれてしまいます**。それを防ぐための設定をします。

アプリの「パッケージの内容を表示」で、中身を見ることができます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/fecd4afa-3bb7-4b15-1e4e-c01fa21b427b.png)

そこにある、 `Packaget.swift` に設定を追加します。


```.swift
/* 略 */

targets: [
    .executableTarget(
        name: "AppModule",
        dependencies: [
            .product(name: "PlaygroundTester", package: "playgroundtester")
        ],
        path: ".",
        swiftSettings: [
            .define("TESTING_ENABLED", .when(configuration: .debug)) // これ
        ]
    )
]

/* 略 */
```

これで導入は完了です。

## 3. 実装

Xcode のテストとは違い、プロダクションコードで実行する必要があります。
その**結果を View で表示して確認できる**ようにしたのが、このライブラリの特徴です。


```.swift
struct Myapp: App {
    init() {
        PlaygroundTester.PlaygroundTesterConfiguration.isTesting = true
    }
    var body: some Scene {
        WindowGroup {
            PlaygroundTester.PlaygroundTesterWrapperView {
                // YourContentView()
            }
        }
    }
}
```

フラグを `false` にすれば自身が作成した `View` を閲覧できます。仮に `true` のまま間違ってストアに上げても、内部的にはアプリの `View` を返すようになっているのでそこは安心です。


実際の例を交えて見ていきます。
ここでは、以下のモデルをテストする場合を想定していきます。

```.swift
class Model {
    var counter = 0
    
    func add() {
        counter += 1
    }
    
    func remove() {
        counter -= 1
    }
}
```

テストコードはこのようになります。

```.swift
import PlaygroundTester // <- テストできるようにする

@objcMembers // <- これがないとテストが動かない
final class ModelTest: TestCase {
    
    func testAdd() {
        let model = Model()
        AssertEqual(model.counter, other: 0)
        
        model.add()
        AssertEqual(model.counter, other: 1)
    } 
    
    func testRemove() {
        let model = Model()
        AssertEqual(model.counter, other: 0)
        
        model.remove()
        AssertEqual(model.counter, other: -1)
    } 
}
```

- `PlaygroundTester` の `import` をする
- テストクラスに `@objcMembers` をつける

を忘れないようにしましょう。

また、その際の `View` ではこのように結果を確認できます。
![view](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/4d15361a-5862-26fa-8fd6-ab8e983d533f.png)


Xcode のテストと同様に `testXXX` となっている関数のみがテストとして実行されます。
また、Assert 系も一通りあるので、Xcode のテストと同じような書き方を実現できます。

```.swift
public func Assert(_ value: Bool, message: String = "")
public func AssertFalse(_ value: Bool, message: String = "")
public func AssertEqual<T: Equatable>(_ value: T, other: T, message: String = "") 
public func AssertNotEqual<T: Equatable>(_ value: T, other: T, message: String = "")
public func AssertNil<T>(_ value: T?, message: String = "")
public func AssertNotNil<T>(_ value: T?, message: String = "")
public func AssertUnwrap<T>(_ value: T?, message: String = "") throws -> T
public func AssertExpectations(_ expectations: [Expectation], timeout: TimeInterval)
```

全てあるわけではないので、アップデートを待ちましょう。
（またはPRを投げましょう！）

ちなみにライブラリの実装的に
`AssertEqual(期待値, other: 受け取った値)`
のようですが、違和感があるので自分は逆に実装しています。

## 4. 出来ないこと

個人で開発されているライブラリのため、機能がまだまだ浅い面もあります。

リポジトリにも書かれていますが

- Combine のサポート
- async/await のサポート

このあたりは後回しのようです。

少し冗長ではありますが、`Expectation` はあるので、Comebine　はしばらくはそれで書くしかなさそうです。

ロードマップがあり、Github の issue としては積まれているので、気長に待つかPRを投げましょう。

# 終わりに
今回は自前でテストコードを書く方法を紹介しました。

`Swift Playgrounds` はアップデートされるたびに出来ることがかなり増えています。
なので、近い将来サポートされることを願いつつ...といったところでしょう。


## リポジトリ


以下に導入したアプリがあるので、参考程度に見ていただけますと🙏

https://github.com/crane-hiromu/PadOSDC_Japan_App


## その他

関連記事はこちら

https://qiita.com/hcrane/items/4b287e9b9cddd0106572

https://qiita.com/hcrane/items/7a422bdf76091befc8c4

https://qiita.com/hcrane/items/7740c0718b6516b2393c