複数のテストバンドルに分かれたXCTestのカバレッジをマージする

# 概要

この記事では、XCTestのカバレッジデータをコマンドライン上で操作する方法に関して解説します。
カバレッジデータを操作するには専用のデータ(.xcresult)とコマンド操作(xccov, xcresulttools)が必要となります。
また、具体的な応用事例として　**「複数に分かれたテストバンドルのカバレッジデータをマージして総カバレッジを出す」** というのを行います。

# カバレッジの解析に必要な道具を揃える・理解する

まず初めに、カバレッジをコマンドラインで操作するために必要な道具について解説します。

## .xcresultファイルを取得する
カバレッジデータを生成するにはXcodeでテストカバレッジ計測をオンにする必要があります。
まず、Scheme一覧のEdit SchemeからTestのタブを開いてチェックをつけます。

![スクリーンショット 2022-02-26 23.06.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29477/8b3441b4-72a8-2700-c9ab-4a884b920a82.png)

その後Cmd+Uでテストを実行すると、テストカバレッジが生成されるようになります。
テストカバレッジはProject Navigatorの「Show Report Navigator」から確認できます。

![スクリーンショット 2022-02-26 23.10.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29477/ce78ae0e-6df4-e7c4-1be7-60970243adc2.png)

右クリックをして「Show in Finder」をクリックするとファイルがある場所に移ることができます。

![スクリーンショット 2022-02-27 17.58.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29477/d10605d0-1a60-d215-91e2-5e3e1c695d45.png)

テストカバレッジのデータは```.xcresult```というパッケージで保存されており、このファイルはコマンドラインからの扱えます。

解析するには以下の２つのコマンドを使用します。
役割は似ていて結構同じことができるのですが、少しずつ違いがあるためどちらも使えるようになっておいた方がいいでしょう。
- xccov
- xcresulttool

2つとも```xcrun```を経由して実行します。

### xcresulttoolの使い方
ドキュメントは用意されていません(manページのみ)。何ができるかの詳細は```help```サブコマンドで調べていきます。

```
xcrun xcresulttool --help
```

helpサブコマンドを打つと使えるサブコマンド一覧が出力されるので、さらにそれらに対して```--help```を実行して役割や使い方を調べていきます。

```
xcrun xcresulttool get --help
```

getに対して使用可能なオプションが見つかるのでそれらを使って実行しています。

```
xcrun xcresulttool get --path sample.xcresult --format json
```

このコマンドを実行するとjson形式でテストカバレッジのメタデータが出力されます。

### xccov

ドキュメントは用意されていません(manページのみ)。何ができるかの詳細は```help```サブコマンドで調べていきます。

```
xcrun xccov help
```

helpサブコマンドを打つと使えるサブコマンドとそれらの使い方がわかるので、結果をもとに実行します

```
xcrun xccov view --archive --file-list export.xcresult
```

このコマンドはファイル単位でのカバレッジをリストで出力します。

# .xcresultから.xccovarchiveと.xccovreportの生成

```.xcresult```を直接扱うこともできるのですが、より解析しやすくするため```.xccovarchive```と```.xccovreport```の２つを生成します。```.xccovarchive```にはファイルの各行に関する細かいカバレッジデータが含まれており、```.xccovreport```はメタデータを扱います。

作業は少し癖があって最初はとっつきにくいですが、慣れれば簡単です。

## idを探し当てる

上述の２つを生成するためには```.xcresult```内にある**ID**を取得する必要があります。

```.xcresult```はディレクトリで、その中にはInfo.plistと分割されたバイナリデータがあります。
バイナリデータはそのままでは読むことができないのでこれを読める形に復元します。

```
sample.xcresult
├── Data
│   ├── data.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw==
│   ├── data.0~5jnweIyI7KJZYMCxddH5dlK1FAr0xdWnIQCNLIKNRHATSI_gxPVdo2kkhDEgt14ciE9NBe7UEem64nYPiiicIA==
│   ├── data.0~6ISB5n3exwjH0YZeXQqQ-AAavppAXZKqnIW5unTxqbGwhFic4mXBF_q4dYFN8D3p_li1EwkrJjPGFRE-l_oo7Q==
│   ├── data.0~8X6LHAW4WpROz2od-hssStrzKjuJooIzdqLoV4BY_u33BC-TNAqYiEuomvjlkWn982zIWbX2GmiCmbtEA_tNiw==
│   ├── data.0~DUsyWDayE2_xnv8PuhnvGOveEo-6e8Usv-j2DKz9ana8rR2JslxjMgR0jxOX9Iz2Y8BF9wQvxQlTzspJJv9w4g==
│   ├── refs.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw==
│   ├── refs.0~5jnweIyI7KJZYMCxddH5dlK1FAr0xdWnIQCNLIKNRHATSI_gxPVdo2kkhDEgt14ciE9NBe7UEem64nYPiiicIA==
│   ├── refs.0~6ISB5n3exwjH0YZeXQqQ-AAavppAXZKqnIW5unTxqbGwhFic4mXBF_q4dYFN8D3p_li1EwkrJjPGFRE-l_oo7Q==
│   ├── refs.0~8X6LHAW4WpROz2od-hssStrzKjuJooIzdqLoV4BY_u33BC-TNAqYiEuomvjlkWn982zIWbX2GmiCmbtEA_tNiw==
│   ├── refs.0~DUsyWDayE2_xnv8PuhnvGOveEo-6e8Usv-j2DKz9ana8rR2JslxjMgR0jxOX9Iz2Y8BF9wQvxQlTzspJJv9w4g==
│   ├── refs.0~Ho4URfFpOlkF_yunDTGIHD26BYRVMIseiNWjFLYEDyBwSdrJ7PDkdieHERv22_n_2ektzRNJ2jMDGHr3o2QIaQ==
└── Info.plist
```

以下のコマンドで```sample.xcresult```をjson形式で見ることができます。

```
xcrun xcresulttool get --path sample.xcresult --format json 
```

出力結果の行数がかなり長いため、どこに何が書いてあるのか構造が理解しにくいでしょう。
```.xcresult```の構造がどうなっているのかを確認するためだけのサブコマンドも用意されています。
それを叩いて全体像を確認しましょう。

```
xcrun xcresulttool formatDescription get 
```

とはいえ、これはこれでかなり項目が多くわかりにくいと思います。
それぞれ何を示しているのかは、目的に応じて都度実際に記載されている内容と照らし合わせながら推測していくしかないです。

ここで必要とされている```.xccovreport```と```.xccovarchive```を生成するための**ID**だけでいえば、以下の項目を見れば良いです。

```
  - CodeCoverageInfo
    * Kind: object
    * Properties:
      + hasCoverageData: Bool
      + reportRef: Reference?
      + archiveRef: Reference?
```

この```reportRef```と```archiveRef```にそれぞれのIDが記載されています。

それではsample.xcresultの該当する項目を見てみましょう。

```
...
...
        "actionResult" : { 
          "_type" : { 
            "_name" : "ActionResult"
          },  
          "coverage" : { 
            "_type" : { 
              "_name" : "CodeCoverageInfo"
            },  
            "archiveRef" : { 
              "_type" : { 
                "_name" : "Reference"
              },  
              "id" : { 
                "_type" : { 
                  "_name" : "String"
                },  
                "_value" : "0~_8vRmaabmeE6qxk5SPr5-zItlyCmdeOhHEOXQBtDHePMDaWSwSoV1O_YVbeEHQfau3uwQ1RRdKGPf6vVryQOSA=="
              }   
            }, 
            "reportRef" : { 
              "_type" : { 
                "_name" : "Reference"
              },  
              "id" : { 
                "_type" : { 
                  "_name" : "String"
                },  
                "_value" : "0~U9Mb0J7rRAGvBun8pW62sZBA0Heg4CVvkgqWGHO3b7TdzHFcNva6h39z3F0poVgIlJYNFxqvO5ZakFc7MDpNJw=="
              }   
            }   
          }, 
...
...
```

読み方がややこしいのですが、ぞれぞれ```"archiveRef" > "id" > "_value"```と```"reportRef" > "id" > "_value"```　を見れば大丈夫です。

ここでは
```
xcarchiveのID: 
  "0~_8vRmaabmeE6qxk5SPr5-zItlyCmdeOhHEOXQBtDHePMDaWSwSoV1O_YVbeEHQfau3uwQ1RRdKGPf6vVryQOSA=="
xcreportのID:
　　　　"0~U9Mb0J7rRAGvBun8pW62sZBA0Heg4CVvkgqWGHO3b7TdzHFcNva6h39z3F0poVgIlJYNFxqvO5ZakFc7MDpNJw=="
```

です

# .xccovarchiveのエクスポート

xcresulttoolのexportコマンドでエクスポートします。pathにxcresultを指定して、idにxcresultから見つけてきたxcarchiveのidを指定します。xccovarchiveはディレクトリのため、typeにはdirectoryを指定する必要があります。

```
xcrun xcresulttool export --path sample.xcresult --id 0~_8vRmaabmeE6qxk5SPr5-zItlyCmdeOhHEOXQBtDHePMDaWSwSoV1O_YVbeEHQfau3uwQ1RRdKGPf6vVryQOSA== --type directory --output-path export_0.xccovarchive
```

カレントディレクトリにexport_0.xccovarchiveが生成されます。

# .xccovreportのエクスポート

xcresulttoolのexportコマンドでエクスポートします。pathにxcresultを指定して、idにxcresultから見つけてきたreportRefのidを指定します。xccovreportはディレクトリのため、typeにはfileを指定する必要があります。

```
xcrun xcresulttool export --path sample.xcresult --id 0~U9Mb0J7rRAGvBun8pW62sZBA0Heg4CVvkgqWGHO3b7TdzHFcNva6h39z3F0poVgIlJYNFxqvO5ZakFc7MDpNJw== --type file --output-path export_0.xccovreport
```

カレントディレクトリにexport_0.xccovreportが生成されます。

# xccovarchiveとxccovreportからカバレッジ情報を取得する

続いて```.xccovarchive```・```.xccovreport```でできることを見ていきます。

## 特定のファイルに対して、行単位のカバレッジを取得する

以下のコマンドでカバレッジが見れるファイル一覧を出力します。

```
xcrun xccov view --file-list export_0.xccovarchive
```

そのうちのどれかを使って以下のコマンドを打つと、ファイルの各行のカバレッジがどうなっているかが見れます。
アスタリスクがついている部分はカバレッジ計算の対象外となっている部分で、数値はそこを通った回数です。
Xcode上でCode Coverageを表示させた時と同じ形になっています。
```
 1: *
 2: *
 3: *
 4: *
 5: *
 6: *
 7: *
 8: *
 9: *
10: *
11: *
12: *
13: *
14: *
15: *
16: *
17: *
18: *
19: *
20: *
21: *
22: *
23: *
24: *
25: *
26: *
27: *
28: *
29: *
30: *
31: *
32: *
33: *
34: *
35: *
36: *
37: *
38: 49
39: 49
40: 49
41: *
42: *
43: *
44: *
45: 49
46: 49
47: 49
48: *
49: *
50: 0
51: 0
52: 0
53: *
54: *
55: *
56: *
57: 0
58: 0
59: 0
60: 0
61: 0
62: 0
63: 0
64: 0
65: *
66: *
67: *
68: *
69: *
70: *
71: *
72: *
73: 49
74: 49
75: 49
76: 49
77: 49
78: 49
79: 49
80: *
81: *
82: *
83: *
84: *
85: *
86: *
87: 0
88: 0
89: 0
90: 0
91: 0
92: 0
93: 0
94: 0
95: 0
```

## 特定ターゲットのファイル毎のカバレッジを取得する

以下のコマンドでファイル毎のカバレッジを出力します。注意点としては指定するターゲットはパッケージ名です。

```
xcrun xccov view --files-for-target Hoge.framework export_0.xccovreport
```

以下のようにファイル毎のカバレッジを表形式で見ることができます。

```
ID Name                                               # Functions Coverage         
-- -------------------------------------------------- ----------- ---------------- 
0  /Path/Hoge/Sources/Class1.swift                    8           48.72% (19/39)   
1  /Path/Hoge/Sources/Class2.swift                    2           100.00% (24/24)  
```

ファイル単位で出力できるということはカバレッジの計算対象外としたいファイルを除くこともできます。
表形式だと扱いにくいため、```--json```をつけてjson形式にするとプログラムから処理しやすくなるでしょう。

```
xcrun xccov view --files-for-target Hoge.framework export_0.xccovreport --json
```

## ターゲット全体のカバレッジを見る

ターゲット全体のカバレッジを見る場合は以下のコマンドを実行します。
```
xcrun xccov view --only-targets export_0.xccovreport
```

ファイル毎のカバレッジと同じように表形式で見れます。

```
ID Name                   # Source Files Coverage          
-- ---------------------- -------------- ----------------- 
0  Hoge.framework         10             21.07% (326/1547) 
1  HogeTests.xctest       4              32.18% (671/2085) 
```

他にも、関数単位でのカバレッジを出力するなど色々できます。詳細は初めに書いた通り、**help**サブコマンドで確認してください。

# 複数のカバレッジをマージして総カバレッジを確認する

複数のテストバンドルのカバレッジをまとめることもできます。
テスト対象が同じでテストバンドルが複数に分かれているケースなどで利用できます。


前述までで生成した複数の```.xccovreport```, ```.xccovarchive```に対して、**merge**コマンドを使うことで実現できます。

以下のようにmergeコマンドの引数に生成したやつを全部指定してやれば大丈夫です。

```
xcrun xccov merge export_0.xccovreport export_0.xccovarchive export_1.xccovreport export_1.xccovarchive 
```

こうすると、```merged.xcarchive```, ```merged.xcreport```がカレントディレクトリに生成されます。

「xccovarchiveとxccovreportからカバレッジ情報を取得する」で書いている内容と同じことをすれば、複数のテストバンドルの総カバレッジを取得できます。

複数のカバレッジデータを使っできることには、他にもdiffを取るなどもがあります。こちらも```help``` サブコマンドで詳細が確認できます。

# コマンドラインでの出力結果を応用する

このようにカバレッジデータはコマンドライン上で細かく操作することが可能です。
例えば、
**「複数のテストバンドルをマージして、不要なファイルを取り除いた上で通った行数/全行数として、行カバレッジの計算を行う」**
などといったスクリプトも実装できます。
それ以外にも様々な応用が考えられます。


# まとめ
- XCTestのテストカバレッジをコマンドライン上で出力できる
- 複数カバレッジのマージやdiffを取るなどできる
- スクリプトと組み合わせて様々な応用が可能

# 参考資料

Xcodeのテストとしてはこちら
- [Running Tests and Viewing Results](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/05-running_tests.html)

公式の資料としてはWWDC2019のものを見るのが良いです

- [Testing in Xcode](https://developer.apple.com/videos/play/wwdc2019/413/?time=2932)

網羅的なドキュメントはあまり存在していませんが、日本語の資料としては以下が一番詳細に記載されていると思います。

- [メルペイでのxcresult活用事例](https://engineering.mercari.com/blog/entry/20201218-61f7110851/)

あとは各コマンドのヘルプと以下のようなライブラリの実装から読み解いていくのがいいです。

- https://github.com/fastlane-community/xcresult
- https://github.com/ChargePoint/xcparse