StoryboardのViewControllerを生成するExtensionのテストを書く

# 概要

**[使うと手放せなくなるSwift Extension集 (Swift3版)](https://qiita.com/tattn/items/bdce2a589912b489cceb#storyboard%E3%81%AEviewcontroller%E3%82%92%E7%94%9F%E6%88%90)**に書かれている[StoryboardのViewControllerを生成](https://qiita.com/tattn/items/bdce2a589912b489cceb#storyboard%E3%81%AEviewcontroller%E3%82%92%E7%94%9F%E6%88%90)が便利なのでプロジェクトに取り入れました。
その際にテストを書いてみたので備忘録として記載。

ちなみにViewControllerの生成がこんな感じで書けるようになるExtensionです。

```swift:使うと手放せなくなるSwiftExtension集(Swift3版)より
MyViewController.instantiate() // Storyboardファイルとクラスが同じ名前の場合
MyViewController.instantiate(withStoryboard: "MyStoryboard")
```

使い方自体は**[使うと手放せなくなるSwift Extension集 (Swift3版)](https://qiita.com/tattn/items/bdce2a589912b489cceb#storyboard%E3%81%AEviewcontroller%E3%82%92%E7%94%9F%E6%88%90)**をご参照ください。

書くにあたって以下の記事を参考にしました。ありがとうございます。

- [使うと手放せなくなるSwift Extension集 (Swift3版)](https://qiita.com/tattn/items/bdce2a589912b489cceb#storyboard%E3%81%AEviewcontroller%E3%82%92%E7%94%9F%E6%88%90)
- [特定の拡張子のファイルをリスト化](https://qiita.com/eggmobile/items/7f8fdee9ac23aca11bf5)
- [Xcodeプロジェクト内で使っているStoryboardやXibが全てインスタンス化可能な事をテストする](https://qiita.com/horimislime/items/f77bc18ef8ceaeab32d0)
- [Swift Tip String型のクラス名からクラスを生成する。](http://onga-tec.hatenadiary.jp/entry/2016/08/28/Swift_Tip_String%E5%9E%8B%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9%E5%90%8D%E3%81%8B%E3%82%89%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B%E3%80%82)
- [Create a swift2 class from classname](https://stackoverflow.com/questions/33100021/create-a-swift2-class-from-classname/33100108)
- [Objective-C 指定したディレクトリ以下の全ファイルを表示](http://appstars.jp/archive/512)

# 結論

[https://github.com/TakkuMattsu/StoryBoardInstantiatableUnittest](https://github.com/TakkuMattsu/StoryBoardInstantiatableUnittest)に書いたテストコードをあげています。
ややゴリ押し+条件があります。

**2017/10/10 追記**
@okuderap さんからのアドバイスを反映
([以前のコードとの差分はこちら](https://github.com/TakkuMattsu/StoryBoardInstantiatableUnittest/pull/1/commits/633670e3e0cd1053ecc2f2bcfd2a0f6f2ef3d4d0))

```swift
class UIViewController_StoryboardTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
    func test_ストーリボートからViewController作成() {
        // info.plistにViewControllerが置いてる場所のパスがあるのでそれを取得
        guard let infolist: [String : Any] = Bundle(for: UIViewController_StoryboardTests.self).infoDictionary, let vcPath =  infolist["Source Directory"] as? String else {
            XCTFail()
            return
        }
        /// 除外リスト
        let excludeList = [
            ViewController.className          // 起動Storyboardと結びついているので除外
        ]
        /// テスト
        let files = viewControllerFileNames(atPath: vcPath)
        print("ViewController数:\(files.count) うち除外:\(excludeList.count)")
        let testList = files.filter { (file) -> Bool in
            // 除外リスト
            excludeList.map({ (excludeClassName) -> Bool in
                file != "\(excludeClassName).swift"
            }).reduce(true, { (result, bool) -> Bool in
                result && bool
            })
            }.map { (file) in
                let fileName = (file as NSString).deletingPathExtension
                print("\(fileName)生成テスト")
                let className = Bundle.main.infoDictionary!["CFBundleName"] as! String + "." + fileName
                let aClass = NSClassFromString(className) as! UIViewController.Type
                let vc = aClass.instantiate()
                XCTAssertNotNil(vc, "\(className)")
        }
        // 全てテストできているか
        XCTAssertTrue(testList.count == (files.count - excludeList.count))
        print("実施:\(testList.count)")
        print("除外:\(excludeList.count)")
        excludeList.forEach { (file) in
            print("- \(file)")
        }
    }
}

private extension UIViewController_StoryboardTests {
    
    /// 指定ディレクトリ内の「ViewController.swift」が含まれているファイル名を取得
    ///
    /// - Parameter dirPath: ディレクトリパス
    /// - Returns: 「ViewController.swift」が含まれているファイル名の配列
    func viewControllerFileNames(atPath dirPath: String) -> [String] {
        
        var vcFileNames = [String]()
        var isDir: ObjCBool = false
        let vcSuffix = "ViewController.swift"
        let fileExists = FileManager.default.fileExists(atPath: dirPath, isDirectory: &isDir)
        
        if !fileExists {
            XCTFail("dirPath does not exist.")
        }
        if !isDir.boolValue {
            XCTFail("dirPath is not a directory.")
        }
        
        if let paths = FileManager.default.enumerator(atPath: dirPath) {
            while let path = paths.nextObject() as? String {
                if path.hasSuffix(vcSuffix) {
                    let fileName = (path as NSString).lastPathComponent
                    vcFileNames.append(fileName)
                }
            }
        }
        return vcFileNames
    }
}
```

```sh:テスト結果
Test Suite 'UIViewController_StoryboardTests' started at 2017-10-08 23:05:31.800
Test Case '-[StoryBoardInstantiatableUnitTestTests.UIViewController_StoryboardTests test_ストーリボートからViewController作成]' started.
ViewController数:5 うち除外:1
AoiChangViewController生成テスト
HinataChangViewController生成テスト
KaedeSanViewController生成テスト
KokonaChangViewController生成テスト
実施:4
除外:1
- ViewController
Test Case '-[StoryBoardInstantiatableUnitTestTests.UIViewController_StoryboardTests test_ストーリボートからViewController作成]' passed (0.067 seconds).
Test Suite 'UIViewController_StoryboardTests' passed at 2017-10-08 23:05:31.871.
         Executed 1 test, with 0 failures (0 unexpected) in 0.067 (0.071) seconds
Test Suite 'StoryBoardInstantiatableUnitTestTests.xctest' passed at 2017-10-08 23:05:31.873.
         Executed 1 test, with 0 failures (0 unexpected) in 0.067 (0.074) seconds
Test Suite 'All tests' passed at 2017-10-08 23:05:31.876.
         Executed 1 test, with 0 failures (0 unexpected) in 0.067 (0.080) seconds
** TEST SUCCEEDED **
```

## 補足

### やってること

1. info.plistに書かれたViewControllerが配置してあるディレクトリを検索し、ViewControllerクラスの一覧を取得(`getAllFile()`を読んでいる箇所)
2. 除外リストのViewControllerを抜いてクラス名からクラスを作成
3. 作成したクラスの`instantiate()`を呼んでテスト実施

### 条件について

- ViewControllerをあるディレクトリ配下にまとめている
  - 自分の場合はViewControllerディレクトリ配下に配置
- StoryboardとViewControllerを同じ名前にしている
  - [使うと手放せなくなるSwift Extension集 (Swift3版)](https://qiita.com/tattn/items/bdce2a589912b489cceb#storyboard%E3%81%AEviewcontroller%E3%82%92%E7%94%9F%E6%88%90)を使うため
- [Xcodeプロジェクト内で使っているStoryboardやXibが全てインスタンス化可能な事をテストする](https://qiita.com/horimislime/items/f77bc18ef8ceaeab32d0)にあるようにinfo.plistにソースパス用の環境変数を追加

## まとめ

[使うと手放せなくなるSwift Extension集 (Swift3版)](https://qiita.com/tattn/items/bdce2a589912b489cceb#storyboard%E3%81%AEviewcontroller%E3%82%92%E7%94%9F%E6%88%90)にはめちゃくちゃ役立つTipsがたくさんあります。プロジェクトにあったものを選んで利用することで効率があがると思います。