git hooksをトリガにしてfastlaneからユニットテストを実行する

## 大まかな手順

1. Xcodeプロジェクトを作成する
2. Gemfileを設定する
3. Fastfileの雛形を生成する
4. Fastfileに設定を記述する
5. fastlaneによるテスト実行を試みる
6. git hooksをトリガにしてfastlaneからユニットテストを実行する

## 前提

- homebrewをインストール済み
- rbenvをインストール済み
- rubyを設定済み
    - rubyはrbenvでインストールしたものを使用しています
- bundlerをインストール済み
- XcodeはGitで管理する設定にする

##### 記事記載時(2024-02-07)の各ツールのバージョン

```
$ brew --version
Homebrew 4.2.6
$ gem --version
3.5.5
$ rbenv --version
rbenv 1.2.0
$ ruby -v
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22]
$ bundle --version
Bundler version 2.5.5
$ git --version
git version 2.39.3 (Apple Git-145)
$ bundle exec fastlane --version
fastlane installation at path:
/Users/myuser/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/fastlane-2.219.0/bin/fastlane
-----------------------------
...(省略)...
fastlane 2.219.0
```

## Xcodeプロジェクトを作成する
1. 任意のディレクトリにXcodeのプロジェクトを作成する(以下の例では仮に`FastlaneTest`としておきます)
   - Gitリポジトリは作成する
   - ユニットテストターゲットあり
2. ユニットテストを作成する
3. ターミナルを開き、Xcodeプロジェクトファイルが存在するディレクトリをカレントディレクトリにする
   - `ls`コマンドを実行して`FastlaneTest.xcodeproj`ファイルが存在すればよい

## Gemfileを設定する

1. `bundle init`を実行する。→Gemfileが生成される
2. Gemfileに以下を記載する
    ```ruby
    gem "fastlane"
    gem "xcode-install"
    ```
3. `bundle install`を実行する　-> Gemfile.lockが生成される

この時点のファイル階層の概要は以下

```bash
.
├── FastlaneTest
├── FastlaneTest.xcodeproj
├── FastlaneTestTests
├── Gemfile # 生成された
└── Gemfile.lock # 生成された
```

## Fastfileの雛形を生成する
1. `bundle exec fastlane init`を実行する
	1. `What would you like to use fastlane for?`で`4. 🛠  Manual setup - manually setup your project to automate your tasks`を選択する
	2. 後の入力待ちでは`Enter`を押せば良い
2. 以下のファイルが作成されている
   - `fastlane/AppFile`
   - `fastlane/Fastfile`

この時点のファイルツリー概要は以下

```bash
.
├── FastlaneTest
├── FastlaneTest.xcodeproj
├── FastlaneTestTests
├── Gemfile
├── Gemfile.lock
└── fastlane # 生成された
    ├── Appfile # 生成された
    └── Fastfile # 生成された
```


## Fastfileに設定を記述する
1. Fastfileに記述する情報を確認しておく
   - 以下の情報を確認しておく(コロンより後の値は例です。)
      - Xcodeバージョン: `15.2`
      - テスト対象のプロジェクトファイル名: `FastlaneTest.xcodeproj`
      - テスト対象のスキーマ名: `FastlaneTest`
      - テスト対象のコンフィギュレーション名: `Debug`
      - テストに使用するデバイス名: `iPhone 15 Pro`
      - テスト結果を保存したいディレクトリ名(ディレクトリ名は任意で指定する): `test_output` 
      - レーン名(任意で指定する。fastlaneコマンド実行時に使用する): `unittest`
2. Fastfileに以下の様に設定を記述する
   - リファレンス: https://docs.fastlane.tools/actions/run_tests/#parameters
       - 様々なパラメータがあるので、試してみてください

    ```ruby
    default_platform(:ios)
    
    platform :ios do
      before_all do |lane|
        ensure_xcode_version(version: "15.2")
      end
    
      lane :<レーン名> do |options|
        run_tests(
          project: "<テスト対象のプロジェクトファイル名>",
          scheme: "<テスト対象のスキーマ名>",
          configuration: "<テスト対象のコンフィギュレーション名>",
          devices: ["<テストに使用するデバイス名>"],
          output_directory: "<テスト結果を保存したいディレクトリ名>"
        )
      end
    end
    ```
    
    例
    
    ```ruby
    default_platform(:ios)
    
    platform :ios do
      before_all do |lane|
        ensure_xcode_version(version: "15.2")
      end
    
      lane :unittest do |options|
        run_tests(
          project: "FastlaneTest.xcodeproj",
          scheme: "FastlaneTest",
          configuration: "Debug",
          devices: ["iPhone 15 Pro"],
          output_directory: "test_output"
        )
      end
    end
    ```




## fastlaneによるテスト実行を試みる
1. `bundle exec fastlane unittest` を実行する
2. ターミナルに諸々のログが出力される。最終的に`Test Succeeded`が出ればテスト実行完了

```
...(省略)...

[06:10:33]: ▸ Test Succeeded
+------------------------+
|      Test Results      |
+--------------------+---+
| Number of tests    | 1 |
| Number of failures | 0 |
+--------------------+---+


+-------------------------------------------+
|             fastlane summary              |
+------+----------------------+-------------+
| Step | Action               | Time (in s) |
+------+----------------------+-------------+
| 1    | default_platform     | 0           |
| 2    | ensure_xcode_version | 0           |
| 3    | run_tests            | 31          |
+------+----------------------+-------------+

[06:10:37]: fastlane.tools finished successfully 🎉
```

## git hooksをトリガにしてfastlaneからユニットテストを実行する

例として、git hooksのpre-commitで実行することにする
参考: [git hooksを使用して、git commit前に何がしかの処理を実行する](https://qiita.com/wtrmiya/items/766199e21d29ae3a22d4)

1. `.git/hooks/pre-commit`を以下の様に編集する
   - ファイルが無ければ作成して、実行権限を付加しておく
   - 以下の例ではシェルが bashになっていますので、ご自分の環境に合わせてください

    ```
    #!/bin/bash
    bundle exec fastlane unittest
    ```

2. `git commit`を実行する → ユニットテストが実行され、`git commit`も実行される
    ```
    $ git commit -m "fastlane test"
    
    ...(省略)...
    
    [06:48:23]: ▸ Test Succeeded
    +------------------------+
    |      Test Results      |
    +--------------------+---+
    | Number of tests    | 1 |
    | Number of failures | 0 |
    +--------------------+---+
    
    
    +-------------------------------------------+
    |             fastlane summary              |
    +------+----------------------+-------------+
    | Step | Action               | Time (in s) |
    +------+----------------------+-------------+
    | 1    | default_platform     | 0           |
    | 2    | ensure_xcode_version | 0           |
    | 3    | run_tests            | 23          |
    +------+----------------------+-------------+
    
    [06:48:27]: fastlane.tools finished successfully 🎉
    [main 7ef726f] fastlane test
     15 files changed, 549 insertions(+), 213 deletions(-)
    
    ...(省略)...
    
    $
    ```

`git commit`が実行された。

### (参考)失敗するユニットテストが存在すると`git commit`は実行されない

1. 例として失敗するユニットテストを記述する

    ```swift
    import XCTest
    @testable import FastlaneTest
    
    final class FastlaneTestTests: XCTestCase {
    
        func testExample() throws {
            XCTFail()
        }
    }
    ```

2. `git commit`を実行する → 失敗するテストが存在するため、`git commit`は実行されない。

    fastlaneによるテスト実行時、失敗するテストが存在する場合、fastlaneの終了ステータスは0ではない。
    そのため、`git commit`は実行されない。

    ```swift
    $ git commit -m "fastlane test 2"
    
    ...(省略)...
    
    [07:12:01]: ▸ Failing tests:
    [07:12:01]: ▸ 	FastlaneTestTests.testExample()
    [07:12:01]: ▸ ** TEST FAILED **
    
    ...(省略)...
    
    Failing tests:
    	FastlaneTestTests.testExample()
    
    ** TEST FAILED **
    [07:12:04]: Exit status: 65
    +------------------------+
    |      Test Results      |
    +--------------------+---+
    | Number of tests    | 1 |
    | Number of failures | 1 |
    +--------------------+---+
    
    ...(省略)...
    
    [07:12:05]: Tests have failed
    
    +-------------------------------------------+
    |             fastlane summary              |
    +------+----------------------+-------------+
    | Step | Action               | Time (in s) |
    +------+----------------------+-------------+
    | 1    | default_platform     | 0           |
    | 2    | ensure_xcode_version | 0           |
    | 💥   | run_tests            | 33          |
    +------+----------------------+-------------+
    
    [07:12:05]: fastlane finished with errors
    
    [!] Tests have failed
    $
    ```

3. 念のため、fastlaneを直接実行した場合の終了ステータスを確認する → 1(つまり≠ 0) だった

    ```swift
    $ bundle exec fastlane unittest
    
    ...(省略)...
    
    [07:20:02]: fastlane finished with errors
    
    [!] Tests have failed
    $ echo $?
    1
    $
    ```


### (参考)fastlane実行に失敗すると`git commit`は実行されない

1. 例として`.git/hooks/pre-commit`を以下の様に編集する
   - レーン名を間違えておく(正: unittest → 誤: unittes)

   ```
    #!/bin/bash
    bundle exec fastlane unittes
    ```

2. `git commit`を実行する → fastlaneの実行に失敗し、`git commit`は実行されない

    ```
    $ git commit -m "fastlane test 3"
    
    ...(省略)...
    
    [✔] 🚀 
    [07:05:33]: ------------------------------
    [07:05:33]: --- Step: default_platform ---
    [07:05:33]: ------------------------------
    +------------------------+
    |      Lane Context      |
    +------------------+-----+
    | DEFAULT_PLATFORM | ios |
    +------------------+-----+
    [07:05:33]: Could not find lane 'ios unittes'. Available lanes: ios unittest
    
    +---------------------------------------+
    |           fastlane summary            |
    +------+------------------+-------------+
    | Step | Action           | Time (in s) |
    +------+------------------+-------------+
    | 1    | default_platform | 0           |
    +------+------------------+-------------+
    
    [07:05:33]: fastlane finished with errors
    
    [!] Could not find lane 'ios unittes'. Available lanes: ios unittest
    $
    ```


## (参考)`bundle exec fastlane init`実行時のウィザードの表示内容

2024-02-07時点では、fastlaneを実行すると以下の様に表示される

```
$ bundle exec fastlane init
[⠹] 🚀 /Users/myuser/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/httpclient-2.8.3/lib/httpclient/auth.rb:11: warning: mutex_m was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add mutex_m to your Gemfile or gemspec. Also contact author of httpclient-2.8.3 to add mutex_m into its gemspec.
[⠸] 🚀 /Users/myuser/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/highline-2.0.3/lib/highline.rb:17: warning: abbrev was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add abbrev to your Gemfile or gemspec. Also contact author of highline-2.0.3 to add abbrev into its gemspec.
/Users/myuser/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/jwt-2.7.1/lib/jwt/base64.rb:3: warning: base64 was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add base64 to your Gemfile or gemspec. Also contact author of jwt-2.7.1 to add base64 into its gemspec.
[⠼] 🚀 /Users/myuser/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/CFPropertyList-3.0.6/lib/cfpropertylist/rbCFPropertyList.rb:3: warning: kconv is found in nkf, which will no longer be part of the default gems since Ruby 3.4.0. Add nkf to your Gemfile or gemspec. Also contact author of CFPropertyList-3.0.6 to add nkf into its gemspec.
[✔] 🚀 
[✔] Looking for iOS and Android projects in current directory...
[05:39:37]: Created new folder './fastlane'.
[05:39:37]: Detected an iOS/macOS project in the current directory: 'FastlaneTest.xcodeproj'
[05:39:37]: -----------------------------
[05:39:37]: --- Welcome to fastlane 🚀 ---
[05:39:37]: -----------------------------
[05:39:37]: fastlane can help you with all kinds of automation for your mobile app
[05:39:37]: We recommend automating one task first, and then gradually automating more over time
[05:39:37]: What would you like to use fastlane for?
1. 📸  Automate screenshots
2. 👩‍✈️  Automate beta distribution to TestFlight
3. 🚀  Automate App Store distribution
4. 🛠  Manual setup - manually setup your project to automate your tasks
?  4
[05:40:05]: ------------------------------------------------------------
[05:40:05]: --- Setting up fastlane so you can manually configure it ---
[05:40:05]: ------------------------------------------------------------
[05:40:05]: --------------------------------------------------------
[05:40:05]: --- ✅  Successfully generated fastlane configuration ---
[05:40:05]: --------------------------------------------------------
[05:40:05]: Generated Fastfile at path `./fastlane/Fastfile`
[05:40:05]: Generated Appfile at path `./fastlane/Appfile`
[05:40:05]: Gemfile and Gemfile.lock at path `Gemfile`
[05:40:05]: Please check the newly generated configuration files into git along with your project
[05:40:05]: This way everyone in your team can benefit from your fastlane setup
[05:40:05]: Continue by pressing Enter ⏎

[05:40:15]: fastlane will collect the number of errors for each action to detect integration issues
[05:40:15]: No sensitive/private information will be uploaded, more information: https://docs.fastlane.tools/#metrics
[05:40:15]: ----------------------
[05:40:15]: --- fastlane lanes ---
[05:40:15]: ----------------------
[05:40:15]: fastlane uses a `Fastfile` to store the automation configuration
[05:40:15]: Within that, you'll see different lanes.
[05:40:15]: Each is there to automate a different task, like screenshots, code signing, or pushing new releases
[05:40:15]: Continue by pressing Enter ⏎

[05:40:22]: --------------------------------------
[05:40:22]: --- How to customize your Fastfile ---
[05:40:22]: --------------------------------------
[05:40:22]: Use a text editor of your choice to open the newly created Fastfile and take a look
[05:40:22]: You can now edit the available lanes and actions to customize the setup to fit your needs
[05:40:22]: To get a list of all the available actions, open https://docs.fastlane.tools/actions
[05:40:22]: Continue by pressing Enter ⏎

[05:40:26]: ------------------------------
[05:40:26]: --- Where to go from here? ---
[05:40:26]: ------------------------------
[05:40:26]: 📸  Learn more about how to automatically generate localized App Store screenshots:
[05:40:26]: 		https://docs.fastlane.tools/getting-started/ios/screenshots/
[05:40:26]: 👩‍✈️  Learn more about distribution to beta testing services:
[05:40:26]: 		https://docs.fastlane.tools/getting-started/ios/beta-deployment/
[05:40:26]: 🚀  Learn more about how to automate the App Store release process:
[05:40:26]: 		https://docs.fastlane.tools/getting-started/ios/appstore-deployment/
[05:40:26]: 👩‍⚕️  Learn more about how to setup code signing with fastlane
[05:40:26]: 		https://docs.fastlane.tools/codesigning/getting-started/
[05:40:26]: 
[05:40:26]: To try your new fastlane setup, just enter and run
[05:40:26]: $ fastlane custom_lane
$
```