Xamarin.iOS 単体テストを Visual Studio Team Services 上で動作させる方法

## はじめに

この資料は、Xamarin.iOS の Unit Test を VSTS 上で実行する手順について記載します。

- [Xamarin Blog の記事を](https://blog.xamarin.com/running-xamarin-ios-unit-tests-visual-studio-team-services/)ベースに検証を行いました
    - Blog 記事をそのまま検証するだけでなく、工夫を加えている箇所があります
- Simulator 上で単体テストを実施しています

## サンプルコード

検証に利用したソースコードを、GitHub に公開しています。

- [Xamarin-UnitTest-Vsts](https://github.com/tafuji/Xamarin-UnitTest-Vsts)

![Solution](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/317279fd3699dcc8f11c92d1aeb3d9ea7e69d195/01-Solution.png)

今回の検証では、iOS のクラスライブラリを単体テストする場合を考えます。

|プロジェクト名|説明|
|:-----|:-----|
|XamarinUnitTestsVsts.iOS.Service|単体テスト対象のプロジェクト。サンプルでは、```Calculator``` クラスの単体テストを行っています|
|XamarinUnitTestsVsts.iOS.Service.Test|単体テストプロジェクト。サンプルでは、意図的にテストを失敗させています|

## 手順

具体的な手順を記載します。

### Touch.Server.exe をダウンロードする

[Touch.Server.exe](https://github.com/prashantvc/Touch.Server/releases/)  は、Xamarin.iOS 単体テストをサポートするツールで、このツールを利用して単体テストを実行すると、テスト結果のレポートファイルを容易に取り出すことができます。

このツールをダウンロードし、テストプロジェクトのディレクトリに配置します。

![Touch.Server.exe](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/73ca6746917148d06f095f1006052a67e9d94e5e/03-Touch.Server.exe.png)

- 配置するディレクトリは自由です
    - 配置したディレクトリに合わせて、後述する Shell スクリプトを書き換えてください
- VSTS 上で Touch.Server.exe を起動するため、ソースコードのリポジトリにコミットするようにしてください

### 1. Unit Test プロジェクトの設定

ビルド時に以下の環境変数を設定するように、ビルドの設定を修正します。具体的には、プロジェクトの iOS Build の設定に以下の設定を追加します。

|環境変数名|値|
|:-----|:-----|
|NUNIT_ENABLE_XML_OUTPUT|true|
|NUNIT_SKIP_LOG_HEADER|true|

今回は、VSTS 上で Simulator を動作させるため、ビルドの構成は、'Debug','iPhoneSimulator' で、設定を追加しています。

![iOSBuildSettings](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/317279fd3699dcc8f11c92d1aeb3d9ea7e69d195/02-iOSBuildSettings.png)

### 2. Touch.Server を起動する Shell スクリプトを作成する

Touch.Server.exe を起動するための Shell スクリプトを作成します。

```shell
#!/bin/sh
echo 'Output path = ' $1
TEST_RESULT=$1/test_results.xml
echo 'Delete test result'
rm -rf $TEST_RESULT
mono --debug $1/Touch.Server.exe \
--launchsim  ./bin/iPhoneSimulator/Debug/XamarinUnitTestsVsts.iOS.Service.Test.app \
-autoexit \
-skipheader \
-logfile=$TEST_RESULT \
--verbose \
--device=":v2:runtime=com.apple.CoreSimulator.SimRuntime.iOS-11-3,devicetype=com.apple.CoreSimulator.SimDeviceType.iPhone-8"
```

ビルドサーバーの環境によって、```--device``` のオプションの値を適宜修正する必要があります。
検証を行った環境では、iOS 11.3 の iPhone 8 の Simulator を利用しています。

### 3. Visual Studio Team Services のビルド設定

ここでは、VSTS 上のビルドタスクに関する説明を記載します。

#### 0. ビルドの流れ

VSTS で Xamarin.iOS のビルドタスクを作成し、単体テストを実行するために必要なビルドタスクを追加します。今回の検証では、以下の図の (1) ～ (6) のタスクを追加しています。

![BuildTasks](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/61e574fadd0844a3e58733e72590427453290681/04-BuildTasks.png)

|#|VSTS タスク|概要|
|:-----|:-----|:-----|
|1|MSBuild|単体テストプロジェクトをビルドするタスクです|
|2|Shell Script|Touch.Server.exe を起動し、単体テストを実行するための Shell スクリプトを実行するタスクです|
|3|Publish Test Results|テストレポートのファイルを VSTS に登録するタスクです|
|4|Set variable with value from XML|テストレポートファイルから、エラー件数を取得し、VSTS の変数にその値を格納するタスクです|
|5|Set variable with value from XML|テストレポートファイルから、失敗の件数を取得し、VSTS の変数にその値を格納するタスクです|
|6|Command Line|テストレポートに、エラー、失敗があった場合にビルドを失敗させるためのタスクです|

\#4 ～ \#6 のタスクが、参考にした Xamarin Blog に工夫を加えた点です。実は、VSTS の "Publish Test Reslts" タスクは、失敗したテストケース結果が含まれるレポートを登録しても、ビルドタスクを成功扱いにしてしまいます。参考にした記事ではこの点については、[GitHub](https://github.com/Microsoft/vsts-tasks/issues/1268) で議論されている最中だと述べるにとどめています。

そこで、本記事では、以下に記載する方法で、テストレポートにテストケースの失敗やエラーがあれば、ビルドが失敗扱いとなるようにビルドタスクを組み込んでいます。

- 単体テスト完了後、テストレポートファイルを解析する
- テストレポートにエラーが含まれていたら、ビルドタスクを失敗させる

##### テストレポートファイルの解析

テストレポートを解析する方法には、いろいろな方法が考えられます。テストレポートのファイルは、XML ファイルであり、```test-results``` 要素の属性にテストの成功、失敗などの件数が格納されていますので、この値を VSTS のビルドタスクで取得する方法を考えます。

```xml
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--This file represents the results of running a test suite-->
<test-results
    name=""
    total="5"
    errors="0"
    failures="2"
    not-run="1"
    inconclusive="0"
    ignored="1"
    skipped="0"
    invalid="0"
    date="2018-04-19" time="08:28:42">
    ... 省略
</test-results>
```

今回の検証では、VSTS の拡張機能（[Variables Tasks Pack](https://marketplace.visualstudio.com/items?itemName=YodLabs.VariableTasks)）を利用して、```test-results``` 要素の ```errors```, ```failures``` 属性の値を取得し、VSTS の変数に格納し、後述の Command Line タスクでテストの成功・失敗の判定に利用しています。

##### Command Line タスク

VSTS で提供される Command Line タスクは、標準エラーに出力を行った場合には、タスクをエラー扱いとすることができます。

Command Line タスクの Script で、```echo Failed 1>&2``` のように標準ラー出力を行い、'Advanced' の設定で 'Faile on Standard Error' にチェックを入れることで標準エラーに何らかの出力を行うとタスクをエラーとすることができます。

![FailureCommand1](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/10-BuildFailsCommandTask.png)

テストレポートにエラーが含まれるという条件判定には、VSTS の Control Option の Custom condition を利用します。

![FailureCommand2](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/11-BuildFailsCommandTask.png)

標準エラー出力を行って、ビルドタスクを失敗される方法は、以下のサイトを参考にしました。

- [How to conditionally fail a build in a pull requests with TFS/VSTS](http://hermit.no/conditionally-fail-build-pull-requests-tfs-vsts/)

#### 1. ビルドタスク

MSBuild タスクを利用して、ユニットテスト対象のプロジェクトをビルドします。ビルド対象のプロジェクトに単体テストプロジェクトを指定します。

- ```Project```
    - 単体テストプロジェクトのプロジェクトファイルを指定します

![Build-iOS](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/05-Build-iOS-Project.png)

#### 2. Shell Scrpt を実行する

Shell Script タスクを利用して、Touch.Server.exe を起動する Shell スクリプトを実行します。

- ```Script Path```
    - Touch.Server.exe を起動する Shell Script ファイルを指定します
- ```Arguments```
    - 単体テストのルートパスを指定します

![ScriptTask](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/a7c76306c9a0008f829fd9a9e189b949baa23389/06-ShellScriptTask.png)

#### 3. テストレポートの登録

Publish Test Results タスクを利用して、テストレポートを VSTS に登録します。

- ```Test results files```
    - テストレポートのファイルを指定します
- ```Search folder```
    - テストレポートが出力されるフォルダを指定します
        - レポートの出力フォルダは、Touch.Server.exe を起動する Shell スクリプトを参照してください

![PublishTestResults](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/07-PublishTestResults.png)

#### 4. エラー件数の値の取得と VSTS 変数への格納

Set variable with value from XML ビルドタスク（拡張機能）を利用して、レポートファイルを解析し、エラーの件数を VSTS の変数に格納します。

- ```Variable Name```
    - 取得した値を格納する変数名を指定します
        - 今回の検証では、```NUnitTestResult.Erros``` という名前の変数に値を格納しています
- ```XPath expression```
    - 取得したい XML の要素（属性）を XPath 形式で記述します。ここでは、 ```errors``` 属性の値を取得しています
- ```XML file path```
    - 単体テストレポート XML ファイルをフルパスで指定します

![GetErrorCount](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/08-GetErrorCountsFromTestReports.png)

#### 5. 失敗した件数の値の取得と VSTS 変数への格納

Set variable with value from XML ビルドタスク（拡張機能）を利用して、レポートファイルを解析し、テスト失敗の件数を VSTS の変数に格納します。

- ```Variable Name```
    - 取得した値を格納する変数名を指定します
        - 今回の検証では、```NUnitTestResult.Failures``` という名前の変数に値を格納しています
- ```XPath expression```
    - 取得したい XML の要素（属性）を XPath 形式で記述します。ここでは、 ```failures``` 属性の値を取得しています
- ```XML file path```
    - 単体テストレポート XML ファイルをフルパスで指定します

![GetFailuresCount](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/09-GetFailursCountsFromTestReports.png)

#### 6. テストレポートにエラーが含まれていた場合に、ビルドを失敗にする

Command Line タスクを利用して、単体テストのレポートにエラー・失敗が含まれていた場合は、ビルドタスクを失敗させます。

- ```Script```
    - 標準エラーにメッセージを出力します
- ```Advanced``` の設定で ```Fail on Standard Error``` にチェックを入れます

![FailureCommand1](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/10-BuildFailsCommandTask.png)

- ```Control Options```
    - ```Run this task``` で ```Custom conditions``` を選択します
    - ```Custom Condition``` で以下の条件を入力します
        - 以下のいずれかの場合にタスクが失敗します
            - すでにビルドが失敗している場合
            - ```NUnitTestResult.Erros``` 変数の値が 0 でない場合
            - ```NUnitTestResult.Failures``` 変数の値が 0 でない場合

```vsts
or(failed(), ne(variables['NUnitTestResult.Erros'], '0'), ne(variables['NUnitTestResult.Failures'], '0'))
```

![FailureCommand2](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/11-BuildFailsCommandTask.png)

VSTS の Custom condition に関する詳細は、以下を参照してください。

- [Specify conditions for running a task](https://docs.microsoft.com/en-us/vsts/build-release/concepts/process/conditions?view=vsts)

### 実行

VSTS 上でビルドタスクを実行すると、単体テストが実行され、テスト結果のレポートが登録されることがわかります。

![BuildResults](https://gist.githubusercontent.com/tafuji/0114a904154f3c0c30d57b6c61f98514/raw/42bb80fc03c9091f835b4beede8bdde9ae260ac4/12-BuildResults.png)

## おわりに

Touch.Server.exe を利用することで、iOS の単体テストプロジェクトを自動ビルドプロセスの中に組み込むことができるようになることが確認できました。

Android の場合はどうなるのかと思う方がいるかもしれませんが、Android 向けに [Touch Server For Android](https://github.com/amatkivskiy/Touch.Server.Android) というツールが GitHub で公開されています。このツールを利用した VSTS 上での単体テストの実行方法については、後日検証してみたいと考えています。

## 参考

- サンプルコード
    - [Xamarin-UnitTest-Vsts](https://github.com/tafuji/Xamarin-UnitTest-Vsts)
- 参考にした Blog
    - [Running Xamarin.iOS Unit Tests in Visual Studio Team Services](https://blog.xamarin.com/running-xamarin-ios-unit-tests-visual-studio-team-services/)
    - [How to conditionally fail a build in a pull requests with TFS/VSTS](http://hermit.no/conditionally-fail-build-pull-requests-tfs-vsts/)
    - [Touch.Server.exe](https://github.com/prashantvc/Touch.Server/releases/)