メルペイでのxcresult活用事例

メルペイでのxcresult活用事例

Merpay Advent Calendar 2020 の19日目は、メルペイ iOS チーム EM(Engineering Manager)の 玉城がお送りします。

2020年はメルペイではBackend/Frontend/iOS/Androidの領域で、より素早いリリースイテレーションを達成するためにリグレッションテストの自動化が推進された年でした。
また、これまでスプレッドシートなどで管理されていたテストの項目、実績管理などもTestRailというテスト管理サービスに移行させることでテストの自動化との親和性が上がりつつあります。
私が持つiOSチームではリグレッションテストをXCUITestで自動化する取り組みをしていました。本記事ではXCUITestで作成したテストがどのように管理・サービス連携されているのか、そして連携するデータの元となっているResult Bundleの構造や、情報の取り出し方について説明します。

XCUITestを使ったUIテストの作成については弊社のエンジニアがiOSDCにて発表していますのでこちらのスライド動画も是非ご覧ください。

メルペイのUIテストの全体像

メルペイでのUIテストの全体像について説明します。
はじめに特定のラベルのついたPRへのPushや夜間にスケジュールされたトリガによってCircleCIのワークフローが起動します。次にワークフローの中でテストが実行され、その結果をTestRailに連携しています。

TestRailとは

TestRailとはテスト管理ツールでテストケース、テストの実行履歴、実行結果の管理などを行えるツールです。APIが提供されておりプログラムからTestRailの操作することも出来ます。
メルペイではモバイルアプリのリグレッションテストについて手動・自動のいずれもTestRailで管理を行っています。
TestRail https://www.gurock.com/testrail/
TestRail API https://www.gurock.com/testrail/docs/api

TestRailとの連携

XCUITestの実行結果をTestRailに連携するためにメルペイではfastlaneのカスタムアクションを作成しています。カスタムアクションではResult Bundleを元にTestRailに連携するために必要な情報を取出し、データを整形してTestRail APIを使って連携しています。

カスタムアクションの中でどのようにResult Bundleの内容を取出しているのか?
それを理解するためにはまずResult Bundleについて詳しく知る必要があります。

Result Bundleを開いてみよう

早速Result BundleをXcodeで開いて内容を確認してみましょう。

XcodeのReport Navigatorタブから任意のテストのログを右クリック→Show in Finderを選択することでFinderアプリ上でResult Bundleである*.xcresultファイルが確認できます。

Xcodeでテストを実行するとResult Bundleは $HOME/Library/Developer/Xcode/DerivedData/${プロジェクト名-xxx}/Logs/Testに生成されます。

またxcodebuildコマンドでビルドを行ってる場合はxcodebuild-resultBundlePathオプションを使って任意の場所にResult Bundleを出力して確認してみると良いでしょう。

xcresultとは

xcresultとはXcode11で採用された新しいResult Bundleのフォーマットです。
Result Bundleには以下のような情報が含まれています。

  • ビルドログ
  • テストレポート
  • テストカバレッジ
  • アタッチメント

パッケージの中身はplistとバイナリファイルで構成されているため、テキストエディタなどで内容を照会することは出来ません。

Result Bundleの内容を確認する

Resule BundleはXcodeを使って内容を確認できます。
xcresultファイルをダブルクリックして開いてみましょう。Report Navigatorタブのみ情報が表示されます。

Result Bundleの情報にアクセスする

Result Bundleの内容を人が読むだけであればXcodeで開く方法で十分かもしれません。しかしプログラムから結果を取り出したい場合には不向きです。そのような場合はxcresulttoolを使って情報を取り出します。
xcresulttoolはAppleから提供されている、xcresultを人が読める、または機械的に解析が可能な形式に変換できるツールです。

使い方はxcrun xcresulttool --helpまたはman xcresulttoolで調べることができますが、本記事ではその内容に更にメルペイでの利用方法を例に添えて以下の4つのサブコマンドについて説明します。

  • get
  • formatDescription
  • export
  • merge

get

getはxcresultの内容を出力するサブコマンドです。基本的にはこのコマンドを使ってxcresultからデータを取出します。

以下の3つのオプションが用意されています。

オプション 説明
–path 任意のxcresultへのパス
–format 出力フォーマット、JSONフォーマットまたはRAWな形式の2種類の出力をサポートしています --format jsonのように指定します
–id オブジェクトのID 指定しない場合はルートIDが使われます
–version Info.plist がないなどの不完全なものに対しては、バージョンを明示的に指定します。(指定しなければ最新のバージョンが使われます)

以下のようにgetコマンドを実行するとxcresultのルートの階層の情報を得ることが出来ます。
※出力結果は長いので一部省略してあります。

$ xcrun xcresulttool get --path report.xcresult/ --format json

{
  "_type": {
    "_name": "ActionsInvocationRecord"
  },
  "actions": {
    "_type": {…},
    "_values": […]
  },
  "issues": {
    "_type": {
      "_name": "ResultIssueSummaries"
    },
    "testFailureSummaries": {
      "_type": {…},
      "_values": […]
    }
  },
  "metadataRef": {…},
  "metrics": {…}
}

getサブコマンドで得られた出力を理解するには次に説明するformatDescriptionサブコマンドで得られる情報からスキーマを理解する必要があります。

formatDescription

formatDescriptionサブコマンドはResult Bundleの形式記述(Format Description)を得ることができます。データがどのような型や構造を持ってるかを確認することができます。

formatDescriptionサブコマンドには更に2つのサブコマンドがあります。
サブコマンドを指定しない場合、get がデフォルトで採用されます。

サブコマンド 説明
get Result BundleのFormat Descriptionが表示されます
diff 二つのResult Bundleのフォーマットの差分を表示します
get

getサブコマンドはFormat Descriptionを内容を指定したフォーマットで出力します。

オプション 説明
–format 出力形式をjson、text、markdown、typeHierarchyのうちのいずれかを指定できます
–hash Format Descriptionのハッシュ値のみを出力します
–include-event-stream-types イベントストリームタイプの型を含めて出力します
–version Result Bundleが不完全な場合(Info.plistがないものなど)、バージョンを明示的に指定する必要があります

--formatオプションでjson/text/markdownを指定した場合は定義されたオブジェクトの親タイプや種類(value/array/object)、プロパティなどの情報を得る事ができます。

$ xcrun xcresulttool formatDescription get --format text
Name: Xcode Result Types
Version: 3.26
Signature: XtEXH3GNNNI=
Types:
  - ActionAbstractTestSummary
    * Kind: object
    * Properties:
      + name: String?
      ...
$ xcrun xcresulttool formatDescription get --format json
{
  "signature" : "XtEXH3GNNNI=",
  "types" : [
    {
      "type" : {
        "name" : "ActionAbstractTestSummary"
      },
      "kind" : "object",
      "properties" : [
        {
          "isOptional" : true,
          "wrappedType" : "String",
          "name" : "name",
          "type" : "Optional"
        }
      ]
    },
    ...
$ xcrun xcresulttool formatDescription get --format markdown
# Xcode Result Types Format Description
- version: 3.26
- signature: XtEXH3GNNNI=
## Types
### ActionAbstractTestSummary
- kind: object
- properties
    - name: [String](#user-content-string)?
...
オブジェクトタイプについて

オブジェクトのタイプは全部で58個あり、現状その全てのタイプの意味を調べきれてはいないので基本的なタイプについて抜粋して説明します。
--include-event-stream-typesオプションで更に追加で表示されるイベントストリームタイプを合わせると86個あります。

タイプは大きく以下の3つに別れます。

  1. Value
  2. Array
  3. Object

Valueには5つのプリミティブなタイプが用意されています。

  • Bool
  • Date
  • Double
  • Int
  • String

Arrayには1つのタイプのみ用意されています。

  • Array

Objectにはその他全てのタイプが該当します。
タイプ名やプロパティを持ち、他のタイプに依存します。

TestFailureIssueSummaryのようにスーパータイプにIssueSummaryを持つような継承関係にあるタイプも存在します。

{
  "_type": {
    "_name": "TestFailureIssueSummary",
    "_supertype": {
      "_name": "IssueSummary"
    }
  },
  "documentLocationInCreatingWorkspace": {…},
  "issueType": {…},
  "message": {…},
  "producingTarget": {…},
  "testCaseName": {…}
},

typeHierarchyオプションを指定すると継承関係がわかりやすく表示されます。

$ xcrun xcresulttool formatDescription get --format typeHierarchy
Name: Xcode Result Types
Version: 3.26
Signature: XtEXH3GNNNI=
Types:
    ActionAbstractTestSummary
    ╠══ ActionTestPlanRunSummary
    ╠══ ActionTestSummaryIdentifiableObject
    ║   ╠══ ActionTestMetadata
    ║   ╠══ ActionTestSummary
    ║   ╚══ ActionTestSummaryGroup
    ╚══ ActionTestableSummary
    ActionDeviceRecord
    ...
    ActivityLogAnalyzerStep
    ╠══ ActivityLogAnalyzerControlFlowStep
    ╚══ ActivityLogAnalyzerEventStep
    ActivityLogMessage
    ╠══ ActivityLogAnalyzerResultMessage
    ╚══ ActivityLogAnalyzerWarningMessage
    ActivityLogMessageAnnotation
    ActivityLogSection
    ╠══ ActivityLogCommandInvocationSection
    ╠══ ActivityLogMajorSection
    ║   ╚══ ActivityLogTargetBuildSection
    ╚══ ActivityLogUnitTestSection
    ArchiveInfo
    Array
    Bool
    CodeCoverageInfo
    Date
    DocumentLocation
    Double
    EntityIdentifier
    Int
    IssueSummary
    ╚══ TestFailureIssueSummary
    ObjectID
    Reference
    ...
    String
    TestAssociatedError
    TypeDefinition

Referenceタイプもxcresultを理解するために重要なタイプなので理解しておきましょう。
xcresultのデータ構造は階層構造を持っており、各階層のデータはそれぞれObjectIDを持っています。getサブコマンドを実行する際--idオプションを指定しない場合はルートのオブジェクトIDが指定されます。
ルートからさらに下層の情報を取得するには下層のオブジェクトのidを指定してgetサブコマンドで情報を取り出す必要があります。
Referenceはそのidを持つオブジェクトです。

以下はルートの階層からアクセスできるReferenceタイプのmetadataRefが持つ情報を出力したものです。

$ xcrun xcresulttool get --path foo.xcresult --format json
...
"metadataRef": {
  "_type": {
    "_name": "Reference"
  },
  "id": {
    "_type": {…},
    "_value": "0~ULBiIf-3dcJwhZgohrk-Knco5iKcQyRckwPuMdp1RumEe1JTmeYCaveZTFqeWVqKA5zUXWmhLS7s8T2EqrKYIQ=="
  },
  "targetType": {…}
}
...

metaデータの具体的な情報を取得するためにはReferenceidを指定してgetサブコマンドを実行します。

$ xcrun xcresulttool get --path report.xcresult/ --format json --id 0~ULBiIf-3dcJwhZgohrk-Knco5iKcQyRckwPuMdp1RumEe1JTmeYCaveZTFqeWVqKA5zUXWmhLS7s8T2EqrKYIQ==
{
  "_type": {
    "_name": "ActionsInvocationMetadata"
  },
  "creatingWorkspaceFilePath": {…},
  "schemeIdentifier": {…},
  "uniqueIdentifier": {…}
}

次にメルペイではResult Bundleのデータをどのように取出して利用しているか例を挙げて説明します。

以下はあるテストについての実行結果をXcodeで開いたものと、TestRailにテスト結果として登録された内容です。
TestRailにはテストの実行ログとしてこのような情報をテスト結果として送っています。テストが失敗した場合はこの実行ログを確認しどのような操作が行われたかTestRail上で確認できるようにしています。

xcresulttoolを使って実行ログのはじめに出力されているTestRail-333513という文字列を取出す操作を例にとってみましょう。
getサブコマンドを使ってActionsInvocationRecordオブジェクトを取得します。

$ xcrun xcresulttool get --path sample.xcresult/ --format json
{
  "_type": {
    "_name": "ActionsInvocationRecord"
  },
  "actions": {
    "_type": {…},
    "_values": [
      {
        "_type": {
          "_name": "ActionRecord"
        },
        "actionResult": {
          "_type": {
            "_name": "ActionResult"
          },
          ...
          "testsRef": {
            "_type": {…},
            "id": {
              "_type": {…},
              "_value": "0~FbFAilYKoJZ-Zf7_bs9M9oG7X_jyCa_bhtYyFhM8q-rKiVKVeDeV37PAH-m36KmUqcW5rJoFqEvouS6i1UMWDA=="
            },
            ...
          }
        },
        ...
      },
    ]
  },
  "issues": {…},
  "metadataRef": {…},
  "metrics": {…}
}

テストに関する情報はActionResultのtestRefのidを指定し更にgetを実行しActionTestPlanRunSummariesオブジェクトの情報を取出します。
以下はその実行結果です。長いため必要な情報のみ抜粋しています。

$ xcrun xcresulttool get --path report.xcresult/ --format json --id 0~FbFAilYKoJZ-Zf7_bs9M9oG7X_jyCa_bhtYyFhM8q-rKiVKVeDeV37PAH-m36KmUqcW5rJoFqEvouS6i1UMWDA==

{
  "_type": {
    "_name": "ActionTestPlanRunSummaries"
  },
  "summaries": {
    "_values": [
      {
        "_type": {
          "_name": "ActionTestPlanRunSummary",
        },
        "testableSummaries": {
          "_values": [
            {
              "_type": {
                "_name": "ActionTestableSummary",
              },
              "tests": {
                "_values": [
                  {
                    "_type": {
                      "_name": "ActionTestSummaryGroup",
                    },
                    ...
                    "subtests": {
                      "_values": [
                        {
                          "_type": {
                            "_name": "ActionTestSummaryGroup",
                            "_supertype": {…}
                          },
                          ...
                          "subtests": {
                            "_values": [
                              {
                                "subtests": {
                                  "_type": {…},
                                  "_values": [
                                    {
                                      "_type": {
                                        "_name": "ActionTestMetadata",
                                        "_supertype": {…}
                                      },
                                      ...
                                      "summaryRef": {
                                        "_type": {…},
                                        "id": {
                                          "_type": {…},
                                          "_value": "0~0exjbpv4VFf0hPkx35rEvpvEAtHq7vCvs6B8qbBkrBP6h8rNauobr7XBhqv8P5G_WgqmcFF3auuDVchsNxikAg=="
                                        },
                                      },
                                    }
                                  ]
                                }
                              },
                            ]
                          }
                        }
                      ]
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    ]
  }
}

ActionTestPlanRunSummariesにはテストに関する情報が含まれていますがアクティビティの情報は含まれていません。
アクティビティの情報を得るためには更にActionTestMetadatasummaryRefのidで情報を取り出す必要があります。

$ xcrun xcresulttool get --path report.xcresult/ --format json --id 0~0exjbpv4VFf0hPkx35rEvpvEAtHq7vCvs6B8qbBkrBP6h8rNauobr7XBhqv8P5G_WgqmcFF3auuDVchsNxikAg==
{
  "_type": {
    "_name": "ActionTestSummary",
    "_supertype": {…}
  },
  "activitySummaries": {
    "_values": [
      {
        ...
        "title": {
          "_value": "Start Test at 2020-12-02 21:50:25.147"
        },
        ...
      },
      {
        ...
        "subactivities": {…},
        "title": {
          "_value": "Set Up"
        },
        ...
      },
      {
        ...
        "subactivities": {…},
        "title": {
          "_type": {…},
          "_value": "TestRail-334452"
        },
      },
      {
        ...
        "subactivities": {…},
        "title": {
          "_type": {…},
          "_value": "Tear Down"
        },
        ...
      }
    ]
  },
  ...
}

ActionTestSummaryactivitySummariesからようやくTestRail-334452を取得することができました🎉
subactivitiesプロパティは入れ子になっており、構造化されたアクティビティの情報が入っています。あとはこれを再起的に検索することでTestRailに送ったようなログを作ることができます。

Tips xcresulttoolを使ってjsonを生で見るのはなかなかしんどいものがあります、筆者としてはjqfxを導入すると比較的この作業が楽になるのでオススメです。

$ xcrun xcresulttool get --path report.xcresult/ --format json | fx

export

exportサブコマンドはファイルやディレクトリをxcresultから取り出すためのサブコマンドです。
例えばXCTAttachmentを使ったスクリーンショットなどの成果物を取り出すことができます。

オプション 説明
–id オブジェクトのID 指定しない場合はルートIDが使われます
–output-path 出力するファイルのパス
–path 任意のResult Bundleへのパス
–type 出力するデータのタイプをfileまたはdirecotryで指定します
–version Info.plist がないなどの不完全なものに対しては、バージョンを明示的に指定します。(指定しなければ最新のバージョンが使われます)

例えば以下のように失敗したテストのAttachmentとして保存されているスクリーンショットの取り出しを例にexportコマンドの実行までを説明します。

XCTAttachmentで保存したスクリーンショットはActionTestAttachmentpayloadRefプロパティからObjectIDを取得する必要があります。

getとformatDescriptionの具体例と同様にActionTestSummaryの情報を取得し、subactivitiesで階層を潜りつつ対象のアクティビティのattachmentsからpayloadRefのidを取得し、これをexportサブコマンドの--idオプションで使用します。

$xcrun xcresulttool get --path report.xcresult/ --format json --id 0~JYD4bn188TwEmx_KIIEgCHRUq6bfARALNayFxFUamBQiS-09B1R8wMpef3O5NEZmmDNgADVMTqKrRim2Y12LKg==
{
  "_type": {…},
  "activitySummaries": {
    "_type": {…},
    "_values": [
      {
        "subactivities": {
          "_values": [
            {
              "subactivities": {
                "_values": [
                  {
                    "subactivities": {
                      "_values": [
                        {
                          "attachments": {
                            "_values": [
                              {
                                "payloadRef": {
                                  "_type": {…},
                                  "id": {
                                    "_type": {…},
                                    "_value": "0~tCUck92hUZFq0aRqTyoNxJbnmI-M6UOwrM0ldhGrG7_Tg1oqAgtYgvdpKWRq8L5S0ZWbokz0rIAe_nsAyd5vmA=="
                                  }
                                },
                              }
                            ]
                          },
                        }
                      ]
                    },
                  }
                ]
              },
            }
          ]
        },
      },
    ]
  },
}

$ xcrun xcresulttool export --path sample.xcresult/ --type file --id 0~tCUck92hUZFq0aRqTyoNxJbnmI-M6UOwrM0ldhGrG7_Tg1oqAgtYgvdpKWRq8L5S0ZWbokz0rIAe_nsAyd5vmA== --output-path failed.jpeg

このようにして取得したスクリーンショットの画像をTestRail APIを使ってテスト結果に添付するようにしています。

merge

mergeサブコマンドは複数のResult Bundleを一つにマージすることができます。

オプション 説明
–output-path マージ後のファイルパス
引数 説明
input paths マージ対象のResult Bundleのパス(複数指定)

実行例

$ xcrun xcresulttool merge A.xcresult B.xcresult --output-path AB.xcresult

メルペイではmergeサブコマンドをCircleCIのワークフロー内で並列なジョブでテストを実行した後にResult Bundleをマージするジョブを動かしています。
これは後続で実行するXCTestHtmlReportやTestRail連携でシンプルに単一のResult Bundleを処理の対象として扱うためです。

ワークフロー内では以下の5つのジョブが実行されます。

  1. ビルド(build-for-testingオプションを使用)
  2. iOSバージョンを分けて並列にテスト実行(test-without-buildingオプションを使用)
  3. Result Bundleのマージ
  4. XCTestHTMLReportの実行
  5. TestRailへの連携

XCTestHTMLReportについては過去にメルカリエンジニアリングブログで取り上げたこちらの記事をご覧ください。

まとめ

本記事ではメルペイでの活用例を踏まえながらXcode11で新しくなったResult Bundleのフォーマットxcresultとそれにアクセスするためのツールであるxcresulttoolの使い方を説明しました。実際にはxcresulttoolをそのまま使うのでは使い勝手が悪いのでfastlaneのカスタムアクションを作ってxcresulttoolの実行とjsonのパース処理を行っています。
今後もiOSアプリ開発においてテストの実行結果を他のサービスとの連携する機会は増えるのではないかと思います。そのような時に本記事の紹介が一助になれば幸いです。

メルペイではミッション・バリューに共感できるエンジニアを募集しています。一緒に働ける仲間をお待ちしております。

ソフトウェアエンジニア (QA Test)

明日の Merpay Advent Calendar 2020 はメルペイ VPoEの nozaqです。引き続きお楽しみください。

Appendix

xcresulttoolのその他のコマンドについてご紹介します。

diff

diffサブコマンドは二つのxcreultのフォーマットの差分を得ることが出来ます。

オプション 説明
–format 出力形式をtext、markdownのうちのいずれかを指定できます(デフォルトはtext)
引数 説明
input paths 比較対象の2つjson形式のformatDescriptionへのパス 

以下はXcode11.6とXcode12.2にそれぞれ付属するxcresulttoolで得たformatDescriptionを比較した例です。
このようにバージョンによって新たに追加されたタイプやプロパティなどの情報を確認できます。

$ xcrun xcresulttool formatDescription diff xcode11.6.json  xcode12.2.json
Diff of versions: 3.24 -> 3.26
Changes:
* added type 'SourceCodeLocation'
* added type 'SourceCodeSymbolInfo'
* added type 'TestAssociatedError'
* added type 'SourceCodeContext'
* added type 'SourceCodeFrame'
* type 'ActionTestFailureSummary' added property 'associatedError'
* type 'ActionTestFailureSummary' added property 'isTopLevelFailure'
* type 'ActionTestFailureSummary' added property 'detailedDescription'
* type 'ActionTestFailureSummary' added property 'attachments'
* type 'ActionTestFailureSummary' added property 'uuid'
* type 'ActionTestFailureSummary' added property 'issueType'
* type 'ActionTestFailureSummary' added property 'sourceCodeContext'
* type 'ActionTestFailureSummary' added property 'timestamp'
* type 'ActionTestActivitySummary' added property 'failureSummaryIDs'

graph

graphサブコマンドはResult Bundleのオブジェクトグラフの出力を得ることができます。

オプション 説明
–id オブジェクトのID 指定しない場合はルートIDが使われます
–path 任意のxcresultへのパス
–version Info.plist がないなどの不完全なものに対しては、バージョンを明示的に指定します。(指定しなければ最新のバージョンが使われます)

実行例

$ xcrun xcresulttool graph --path sample.xcresult
* ActionsInvocationRecord
  - Id: 0~OqfQ_defnXPUr_3DFPqtj2eJF4LkQY2DQZUJICKeLtHGO8c-EZfTAkkHxP3VgledkMUWlX_C73KNf-9E6jUILw==
  - Size: 1943188
  - Refs: 7
    * CASTree (file or dir)
    ...
    * ActivityLogSection
    ...
    * raw
    ...
    * ActivityLogSection
    ...
    * ActionsInvocationMetadata
    ...
    * CASTree (file or dir)
    ...
    * ActionTestPlanRunSummaries
      - Id: 0~xaQ4mjZIgOclWs-dX1ZT-blBL-4WfTHqmuKAk2kGggxcDqgbv555nZ05dXbEzhCX55mNHPpbuc_P22-OjeFPqQ==
      - Size: 2095
      - Refs: 1
        * ActionTestSummary
          - Id: 0~fuf1OFdFoB78xMpwRVaHr-OsS66EM7dRKOaEH2pCOMIy-6-LApJ-3QbFWkPX23d3YntMbsceYXWvOAk0qIOEcA==
          - Size: 92000
          - Refs: 20
            * raw
            ...
            * raw

version

versionサブコマンドはxcresulttoolのバージョン情報を得ることが出来ます。

$ xcrun xcresulttool version
xcresulttool version 17017, format version 3.26 (current)
  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加