iOSのユニットテストでAppDelegateのMockを作成する

# はじめに
ユニットテストをするときに、可能な限り**副作用**を減らしていく必要があるかと思います。

ここでいう「副作用」とは、**他の機能の結果が影響して、テスト対象の機能の結果が変わってしまうこと**です。また、正確なカバレッジが取れないこと・サーバーへ負荷をかけてしまうことも副作用であると考えます（｀・ω・´）

今回は、iOSアプリ開発のAppDelegateの副作用をどうやって無くすか考えていきます。

# 前提
- 本記事では、SceneDelegateを使用していないプロジェクトを想定しています。
- Test Doubleでいうと、Fake Object的なものを作りますが、命名はxxxMockにしています。

テストカバレッジの確認から順を追って説明していきますので、
本題のAppDelegateのMock作成だけ確認したい方は
飛ばして[こちら](https://qiita.com/y-okudera/items/bf91374fdb4acfab927c#appdelegate%E3%81%AEmock%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)から読んでいただいても大丈夫ですm(_ _)m

**（2021.08.17追記）**
**SceneDelegateのMockについても記事を書きました！**
**こちらも併せて確認してみてくださいm(_ _)m**

- [**iOSのユニットテストでSceneDelegateのMockを作成する**](https://qiita.com/y-okudera/items/ac344808f038c074c196)

# 新規プロジェクトのテストカバレッジを見てみる

AppDelegateの副作用について実際に確認するために、
作成したばかりのプロジェクトに、カバレッジを見れるように設定してみます。

だいぶ前に[XCTestでカバレッジを確認する](https://qiita.com/y-okudera/items/de574685592fe0bee8ea)という記事を書きましたが、
Xcodeのバージョンも変わって見た目が少し変わっているので、イメージを載せておきます。
（やり方は、ほぼ変わってないです。）

![スクリーンショット 2021-08-15 10.59.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/f7900b54-6d78-665e-e093-2159148fb3e0.png)

Code Coverageにチェックをつけるだけです:point_up:
収集対象は、**all targets**にしています。
Embedded Frameworkなどを導入しているプロジェクトでは、指定Frameworkのカバレッジのみ収集するよう設定することもできます。

新規作成したプロジェクトにカバレッジの設定をして、テストコードを実行してみました。
![スクリーンショット 2021-08-15 10.33.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/72bd5ffc-ad03-749c-83e2-9895d797c89a.png)

SceneDelegateを取り除いて、再度テストコードを実行してみました。
![スクリーンショット 2021-08-15 12.03.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/979e8c32-9074-1004-b9ef-cb574508746a.png)

テストコードではまだ何も検証していないのに、
AppDelegateと最初のViewControllerのカバレッジが100%になりました｡°(´•ω•̥`)°｡

ユニットテスト実行時も、`func application(_:didFinishLaunchingWithOptions:)`が呼び出されて、最初のViewControllerが表示されていることが確認できました。

新規作成したばかりのプロジェクトなので特に処理はありませんが、
実際のプロジェクトで最初の画面のライフサイクルが実行されると
予期しないAPIリクエストが実行されてしまうといったこともあるかと思います。

# ユニットテスト実行中かどうか判定してみる
AppDelegateの`func application(_:didFinishLaunchingWithOptions:)`内で
ユニットテスト実行中かどうか判定する分岐を加えてみました。

```swift:AppDelegate.swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { // 分岐を追加
            print("ユニットテスト実行中")
        } else {
            print("ユニットテスト実行中ではない")
        }
        return true
    }
}
```

ユニットテスト実行中かどうか判定できるようになったので、
ユニットテスト実行中の場合は、最初のViewControllerを差し替えるようにします。

```swift:AppDelegate.swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { // 分岐を追加
            print("ユニットテスト実行中")

            // 追加
            window?.rootViewController = UIViewController()
            window?.makeKeyAndVisible()
        } else {
            print("ユニットテスト実行中ではない")
        }
        return true
    }
}
```
ここで、再度テストを実行してみます。
![スクリーンショット 2021-08-15 13.54.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/a30d3b1a-6fb6-8bf2-a567-520ef09fbb2f.png)

![スクリーンショット 2021-08-15 13.57.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/911bf34e-4796-cc61-26dc-67cd60a19d82.png)

ViewControllerを差し替えたので、ViewController.swiftは、テスト実行時に呼び出されなくなりました。

AppDelegateもテスト実行時かどうか判定をできているので、必要最低限の処理だけ呼ぶようには改善できたのですが、プロダクトコードに本来は不要な分岐が入ってしまうし、AppDelegateのカバレッジは収集されてしまうので、これでは十分ではないです。

# AppDelegateのMockを作成する
ここからが本題です!

AppDelegate内でユニットテスト実行中かどうか分岐するのではなく、
AppDelegate自体をMockに差し替えるようにしていきます。

以下の順に対応をしていきます。

1. main.swiftを追加して、エントリーポイントを定義する
2. AppDelegateからmain属性を取り除く
3. ユニットテストのターゲットにAppDelegateMockを実装する
4. main.swiftでAppDelegateとAppDelegateMockを出し分ける

## main.swiftを追加して、エントリーポイントを定義する

「main.swift」というファイルを追加して、アプリのエントリーポイントを定義していきます。

普段、iOSアプリを開発する際には、AppDelegate.swiftの
`func application(_:didFinishLaunchingWithOptions:)`が起動時に呼び出されることが分かると思います。
これは、AppDelegateのクラス定義の上に`@main`または`UIApplicationMain`属性が定義されているため、コンパイラがアプリのエントリーポイントを生成してくれています。
そのため、「main.swift」ファイルが不要になっています。

Objective-CでiOS開発を経験したことがある方は、「main.m」ファイルを見たことがあるかと思いますが、Swiftでも「main.swift」を作成することで、コードでエントリーポイントを定義することができます。

```swift:main.swift
import UIKit

let appDelegateClass: AnyClass = AppDelegate.self
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(appDelegateClass))
```

参考情報：
　https://developer.apple.com/swift/blog/?id=7

## AppDelegateからmain属性を取り除く
main.swiftを追加すると、AppDelegateでコンパイルエラーが発生するようになります。

![スクリーンショット 2021-08-15 14.57.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/f0b4e8fa-9642-e73f-e6ec-986d9f36bba4.png)

エントリーポイントは、main.swiftで定義されるため、
AppDelegate.swiftから`@main`または`UIApplicationMain`属性を削除することで
エラーを解消することができます。

## ユニットテストのターゲットにAppDelegateMockを実装する
テストターゲットにAppDelegateMock.swift新規作成します。
![スクリーンショット 2021-08-15 20.58.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/ac6de7c5-a3d0-4362-b67c-6bf38ba7affd.png)

実装は、AppDelegate.swiftをベースにします。

```swift:AppDelegateMock.swift
import UIKit

class AppDelegateMock: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        window?.rootViewController = UIViewController()
        window?.makeKeyAndVisible()
        return true
    }
}
```

この状態では、Applicationのターゲットにあるmain.swiftから
ユニットテストのターゲットのAppDelegateMockクラスを参照できないので、
`NSClassFromString(_:)`でクラス名からクラスを取得できるように `@objc`属性を付けます。

```swift:AppDelegateMock.swift
import UIKit

@objc(AppDelegateMock) // 追加
class AppDelegateMock: UIResponder, UIApplicationDelegate {

    // 省略
}
```

参考情報：
　[Objective-C の NSClassFromString で Swift のクラスを生成する](https://qiita.com/dr_yst/items/5838d2b47434e3667424)
　[NSClassFromString(_:)](https://developer.apple.com/documentation/foundation/1395135-nsclassfromstring)

## main.swiftでAppDelegateとAppDelegateMockを出し分ける
テストターゲットに実装したAppDelegateMockを呼び出せるようにmain.swiftを修正をしていきます。

ユニットテスト実行時は、AppDelegateMockがロードされているので
`NSClassFromString("AppDelegateMock")`でAppDelegateMockのクラスオブジェクトが取得できます。
一方、プロダクトコード実行時は、`NSClassFromString("AppDelegateMock")`がnilになるので、
その場合は`AppDelegate.self`を使用するようにします。

```swift:main.swift
import UIKit

let appDelegateClass: AnyClass = NSClassFromString("AppDelegateMock") ?? AppDelegate.self
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(appDelegateClass))
```

これで、テスト実行時はAppDelegateMockを呼び出せるようになったので
あらためてテストコードを実行してみます。

![スクリーンショット 2021-08-16 0.18.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103695/4e4457e4-1084-3dcd-8b69-17bb510ff566.png)

ユニットテスト実行時にAppDelegate.swiftが実行されなくなりました:metal:

# ソースコード全体
実装したソースコードをまとめると、最終的に以下のようになります。

## Applicationのターゲット

```swift:main.swift
import UIKit

let appDelegateClass: AnyClass = NSClassFromString("AppDelegateMock") ?? AppDelegate.self
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(appDelegateClass))
```

```swift:AppDelegate.swift
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }
}

```
## ユニットテストのターゲット

```swift:AppDelegateMock.swift
import UIKit

@objc(AppDelegateMock)
class AppDelegateMock: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        window?.rootViewController = UIViewController()
        window?.makeKeyAndVisible()
        return true
    }
}
```

# さいごに
今回は、AppDelegateのMockを作成してみました。
本物のAppDelegateを汚さなくて済むようにできましたし、
普段iOSアプリ開発であまり意識する必要がなかったエントリーポイントについても
おさらいすることができてよかったです( ´ ▽ ` )ﾉ