CI で実行した Xcode のテスト結果を HTMLに出力する

iOSアプリをCIでビルドしたりテストするのに　fastlane を使っています。fastlane には [snapshot](https://docs.fastlane.tools/actions/snapshot/) というアクションが用意されており、UIテスト中にスクリーンショットを撮影して結果をHTMLで見ることができます。
便利なのですが、[Xcode 9 から XCUITest自体にスクリーンショットを取得する機能が導入されました](https://developer.apple.com/documentation/xctest/activities_and_attachments/adding_attachments_to_tests_and_activities?language=objc)。

snapshot の代わりにこちら使っていこうと思ったのですが、snapshot を使わないと fastlane でテストを実行したときにスクリーンショットを含んだ HTML ファイルを生成することが現状できないんですよね……。そこで、作ってみました。使い方は README.md を読んでください。

- https://github.com/taisukeh/xcode-test-reporter
- https://github.com/taisukeh/fastlane-plugin-xcode_test_reporter

こんな感じのHTMLができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/49589/9ad88353-5118-91b9-fdc0-77b8eece96cc.png)

以下は実装のメモになります！

## XCUITest でのスクリーンショットの撮影

[XCTAttachment](https://developer.apple.com/documentation/xctest/activities_and_attachments/adding_attachments_to_tests_and_activities?language=objc) を使います。XCUItest で以下のコードを実行します。

```swift
let attachment = XCTAttachment(screenshot: XCUIScreen.main.screenshot())
attachment.lifetime = .keepAlways
attachment.name = "MyScreenshot"
add(attachment)
```

`lifetyme` に　`.keepAlways` を指定するとテスト成功時にもスクリーンショットが残るようになります。デフォルトではテスト失敗時にのみ残ります。`name` にはどの画面かなどの名前を指定します。

テスト中に毎回このコードを書くのは面倒なので、`extension` にメソッドを定義しておくと良いと思います。

```swift
extension XCTestCase {
  func capture(_ name: String, lifeTime: XCTAttachment.Lifetime = .deleteOnSuccess) {
    let attachment = XCTAttachment(screenshot: XCUIScreen.main.screenshot())
    attachment.lifetime = lifeTime
    attachment.name = name
    add(attachment)
  }
}
```

テストを実行すると、テスト結果画面のクリップアイコンからスクリーンショットを見れるようになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/49589/be34ef15-a370-7d8c-5d1e-a5142bfe9be9.png)

テスト結果は `plist` で以下に保存されています。
```
~/Library/Developer/Xcode/DerivedData/<your_app>/Logs/Test
```
またスクリーンショットは
```
~/Library/Developer/Xcode/DerivedData/<your_app>/Logs/Test/Attachments
```
に残っています。plist の中を見るとテストの成否や attachments の情報、実行時間などの情報が記録されており十分な情報がありそうです。

## plist をパースし HTML を生成するツールの作成

https://github.com/taisukeh/xcode-test-reporter

plistをパースする処理は swift なら簡単にかけそう & Codable 使えば楽に書けそうと思ったので swift で作成しました。

### CLI
swift での CLI ツールを作成には [Swift Package Manager](https://swift.org/package-manager/) を使います。

```
swift package init --type executable
````
を実行するとプロジェクトの雛形ができます。あとは `Sources` 以下に実装を `Tests` 以下に書いていきます。

CLI のオプションのパースに使うツールを探してみると、Swift Package Manager に添付されているものを使う方法があるとのことで [ArgumentParserが便利すぎる件 [SwiftPM]](https://qiita.com/masakihori/items/7dfd189bfc83819277be) を使いました。

### plistのパース

plist のパースには Codable を使いました。

```swift
public struct Report: Codable {
   let FormatVersion: String
   let RunDestination: RunDestination
   let TestableSummaries: [TestableSummary]
 }

 public struct RunDestination: Codable {
   let LocalComputer: LocalComputer
   let Name: String
   let TargetArchitecture: String
   let TargetDevice: TargetDevice
 }

public struct TestableSummary: Codable {
   let ProjectPath: String
   let TargetName: String
   let TestName: String
   let Tests: [Test]
 }
...
 ```
このように plist の構造そのままの定義を書いておくと、

```swift
func decodePlist<T>(_ type: T.Type, plistUrl: Foundation.URL) throws -> T where T : Decodable {
   let data = try Data(contentsOf: plistUrl)
   let decoder = PropertyListDecoder()
   return try decoder.decode(type, from: data)
 }

let report: Report = try decodePlist(Report.self, plistUrl: plistUrl)
```
だけでパースが完了します。あとはこの情報を元に HTML を生成するだけです。


### 単体テスト実行

Tests ディレクトリ以下にテストを書くと

```
swift test
```
だけでテストが実行できるのは良いのですが……、よくあるテストツールのようにテスト結果を色付きでわかりやすく表示してくれたりはしません。[xcpretty](https://github.com/supermarin/xcpretty)を使うといい感じになりました。

```
swift test 2>&1 | xcpretty
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/49589/998c59be-872f-6aa3-7e96-e4e83976f227.png)

また今回 CircleCI の siwft docker image でテストを実行するようにしました。Mac上でテストを実行した際は、テストケースの `test` という名前がついたメソッドを自動で見つけて実行くれるのですが、Linux で実行する際には自分でテストメソッドを一覧にしなければなりませんでした。

`LinuxMain.swift` というファイルを用意して、

```swift
import XCTest
 @testable import LibTests

 XCTMain([
     testCase(PlistParserTests.allTests),
 ])
```
各テストクラスで `allTests` を定義しいちいちメソッドを配列にして渡す必要があります。

```swift
class PlistParserTests: XCTestCase {
   override func setUp() {
     super.setUp()
   }

   static var allTests = [
     ("testParse", testParse),
   ]
...
```
これはテストケースが多くなってくると面倒そうですね……。

## fastlane plugin

fastlane には作者直々の [trainer](https://github.com/KrauseFx/trainer) という、Xcodeのテスト結果を JUnit の XML に変換してくれる plugin があります。plist をパースするすればよいということはこの trainer から知りました。今回作成したツールと trainer はやっている処理自体は同じです。HTML を出力するかどうかという違いがあるだけです。

作成した plugin のオプションの指定などは trainer を真似しました。また、JUnit の XML の出力処理も実装しているため、trainer を置き換えることもできます。

## まとめ

snapshot を使わなくても XCUITest でスクリーンショットを撮影 & HTML 出力できるようにしました
Swift Package Manager を使ったツールと、fastlane pluginを作成しました。trainer を置き換える感じで使用できます。

- https://github.com/taisukeh/fastlane-plugin-xcode_test_reporter
- https://github.com/taisukeh/fastlane-plugin-xcode_test_reporter

もし良ければいいねや GitHub のスターをください！

以上、