【QualityForward】自動テスト安定性レポートを簡単に活用できるGitHub Actionsを作りました

自動テストツールのテスト結果をより簡単にQualityForward[^1]の自動テスト安定性レポートに連携できるGitHub Actionsを作りました。
今回は自動テスト安定性レポートの概要と作成したGitHub Actionsの使用方法について説明します。

## サマリ

- 自動テスト安定性レポートは、自動テストの結果や実行時間を時系列で分析・可視化し、自動テストメンテナンスをサポートする
- 今回作成したGitHub Actionsを使用することで、動テスト結果の出力ファイル（json、xml）を簡単にQualityForwardの自動テスト安定性レポートに連携できる

## 自動テスト安定性レポートとは何か

自動テスト安定性レポート[^2]は自動テストの結果や実行時間を時系列で分析し、安定性を可視化することで、自動テスト運用を支援するサービスです。

自動テストには開発スピードの向上、テストの効率性の向上など、プロジェクトに応じてさまざまな目的[^3]があります。しかし、これらの目的を達成するためには自動テストのメンテナンスが不可欠です。メンテナンスが不十分だと、テスト結果の信用性が低下し、形骸化する可能性があります。

たとえば、以下のような項目があげられます。

- フレーキーなテスト[^4]があるかどうか
- 極端に実行時間が増えていないかどうか
- 不要な自動テスト（確認したいことがかぶっている、毎日実施すべきテストではない等）が含まれていないかどうか

このようなメンテナンスを行うためには、日々の自動テスト結果の集計や分析が必要です。そこで、自動テスト安定性レポートを活用すると、日々の集計や分析をより楽に実施でき、自動テストのメンテナンスが容易になります。

### 自動テスト安定性レポートが提供するもの

現在、提供しているものは以下の4つです。

1. 自動テスト全体の結果分布と自動テストスイート[^5]ごとの失敗数分布を円グラフで表現
2. 自動テストスイート×実行環境[^6]ごとの実行回数および累積成功・失敗率の一覧表示
3. 自動テストスイート詳細内のテスト結果分布を円グラフで表現
4. 自動テストスイート×データパターン[^7]ごとのテスト結果の累積、実行時間およびテストサイクル[^8]ごとの結果を一覧表示

各機能の詳細は以下のセクションで説明します。

#### 1. 自動テスト全体の結果分布と自動テストスイートごとの失敗数分布を円グラフで表現

![自動テスト結果分布表示.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1895890/e3c4cb8e-cd34-b5d1-54a2-311987fc10c1.png)

全体の何割が成功/失敗/エラー/スキップしているのか、またどのテストスイートでよく失敗しているのかが可視化され、改善すべきテストスイートを容易に識別できます。

##### QualityForwardのテスト結果ステータスについて

QualityForwardでは自動テスト結果を以下のように定義しています。

| ステータス | 定義 |
| :--: | :-- |
| pass | 自動テスト成功 |
| fail | 自動テスト失敗（アサーションエラー） |
| error | テストの結果判定ができず、なんらかの理由でランタイムエラーが発生 |
| skip | 対象の自動テストケースを実施しなかった |

#### 2. 自動テストスイート×実行環境ごとの実行回数および累積成功・失敗率の一覧表示

![自動テストスイート一覧.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1895890/17b55e00-c0e3-17db-acff-a75f9e2dc72a.png)

実行環境で成功率に差異がないか、よく実行している自動テストスイートはどれかを確認できます。自動テストスイート（auto_test_suite_external_key）ごとにソートして表示されます。

#### 3. 自動テストスイート詳細内のテスト結果分布を円グラフで表現

![テストスイート内の結果分布.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1895890/7a19cdbd-71e7-d2e4-1702-9e8704b60e6b.png)

テストスイート内でどのくらいのテストが成功/失敗/エラー/スキップしているのかを一目で確認できます。

#### 4. 自動テストスイート×データパターンごとのテスト結果の累積、実行時間およびテストサイクルごとの結果を一覧表示

![テストケース一覧.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1895890/51502c20-5e5e-2abc-1314-3172dfae08ae.png)

データパターンごとに成功率や実行時間の差異がないか、断続的に失敗しているテストケースがないかを確認できます。これは、フレーキーなテストの発見に役立ちます。

また、テストサイクルごとの結果に補足メモや結果詳細（ログやSaaSツールの自動テスト結果）のリンクを貼り付けることができ、すぐに詳細を確認できます。テストスイートごとの自動テスト結果は、前回のテストサイクルと比べて実行時間が伸びた場合に赤で強調表現され、自動テストスクリプトの改善のきっかけになります。

さらに、詳細な分析を行うためのcsv出力もあります。

## 自動テスト安定性レポートのデータ投入方法

[QualityForward API](https://qualityforward.github.io/api-spec/#tag/AutoTestResult)をもとに自動テスト結果データを整形し、QualityForwardに連携すると自動テスト安定性レポートに自動テスト結果が表示されます。

### Payload

```json
{
  "auto_test_suite_external_key": "string",
  "auto_test_suite_name": "string",
  "auto_test_cycle_name": "string",
  "auto_execution_device_external_key": "string",
  "auto_execution_device_name": "string",
  "auto_test_results": [
    {
      "auto_test_case_external_key": "string", 
      "auto_test_case_name": "string", 
      "auto_execution_pattern_external_key": "string", 
      "auto_execution_pattern_name": "string",
      "result": "pass",
      "execution_time_taken": 0,
      "info_url": "string",
      "remark": "string" 
    }
  ]
}
```

#### Payloadのプロパティ

|プロパティ|説明|例|
|:--:|:--|:--|
|`auto_test_suite_external_key`|自動テストスイートID（必須）|sample_test_suite|
|`auto_test_suite_name`|自動テスト安定性レポートに表示される自動テストスイート名（任意）<br>未設定の場合は`auto_test_suite_external_key`で設定した値が表示|サンプル自動テストスイート|
|`auto_test_cycle_name`|自動テストサイクル名（必須）|2024-9-28|
|`auto_execution_device_external_key`|自動テスト実施環境ID（任意）|ios17_4|
|`auto_execution_device_name`|自動テスト安定性レポートに表示される環境名（任意）|iOS+17.4|
|`auto_test_results`|自動テスト結果の配列<br>各テストケースの実行結果を格納する|-|
|`auto_test_case_external_key`|自動テストケースID|sample_test_case|
|`auto_test_case_name`|自動テスト安定性レポートに表示される自動テストケース名（任意）<br>未設定の場合は`auto_test_case_external_key`で設定した値が表示|
|`auto_execution_pattern_external_key`|自動テストケースで使用したデータパターンID（任意）<br>データ駆動テストパターンなどで使用する|data_pattern1|
|`auto_execution_pattern_name`|自動テスト安定性レポートに表示されるデータパターン名（任意）|データパターン1|
|`result`|自動テスト結果（必須）|pass|
|`execution_time_taken`|テスト実行時間（単位はミリ秒）|1000|
|`info_url`|自動テスト結果に関する詳細情報が記載されたWebページのURL|https://example.com|
|`remark`|備考|要確認項目あり|

:::note info
詳細は[QualityForwardのAPI仕様書](https://qualityforward.github.io/api-spec/#tag/AutoTestResult)をご確認ください。
:::

### curlコマンドの例

以下はcurlコマンドでデータを連携する例です。

```sh

curl --request POST \
  --header "Content-Type: application/json" \
  --data '{
    "api_key": "[生成したAPIキー]",
    "auto_test_suite_external_key": "sample",
    "auto_test_suite_name": "サンプル自動テスト",
    "auto_test_cycle_name": "2024-07-03",
    "auto_execution_device_external_key": "ios17_4",
    "auto_execution_device_name": "iOS+17.4",
    "auto_test_results": [
      {
        "auto_test_case_external_key": "auto_sample1",
        "result": "pass",
      },
      {
        "auto_test_case_external_key": "auto_sample2",
        "auto_test_case_name": "サンプル自動テストケース2",
        "auto_execution_pattern_external_key": "dataScenario1",
        "auto_execution_pattern_name": "データシナリオ1",
        "result": "fail",
        "execution_time_taken": 100,
        "info_url": "https://example.com",
        "remark": "sample_test2"
      },
        "auto_test_case_external_key": "auto_sample2",
        "auto_test_case_name": "サンプル自動テストケース2",
        "auto_execution_pattern_external_key": "dataScenario2",
        "auto_execution_pattern_name": "データシナリオ2",
        "result": "pass",
        "execution_time_taken": 100,
        "info_url": "https://example.com",
        "remark": "sample_test3"
      }
    ]
  }' \
  "https://cloud.veriserve.co.jp/api/v2/auto_test_suites"

```

自動テスト安定性レポートにデータを投入するためには、自動テスト結果を整形する必要があるため、専用のスクリプトが必要になります。
レポート投入用の準備工数が取れない方向けに、まずは自動テスト安定性レポートを簡単に体験できるよう、GitHub Actionsを作成しました。

https://github.com/QualityForward/test-stability-report-sync

このアクションは、自動テストで出力されたjsonおよびxmlファイルを整形し、QualityForwardの自動テスト安定性レポートに連携します。

## 自動テスト安定性レポート連携アクションの使い方

実際にGitHubActionsを使ったやり方を説明します。

### 大枠な流れ

本アクションを使う流れは以下です。

1. 自動テストツール（OSS or SaaS）で自動テストを実行し、結果をxmlもしくはjsonファイルで出力する
2. アクションを使用して、出力したファイルを読み込みQualityForwardの自動テスト安定性レポートに連携する

### サンプルYamlファイル

以下はpytest-playwrightを使った自動テスト結果をQualityForwardの自動テスト安定性レポートに連携しているyamlファイルです。

:::note info
有償テストツールのサンプルYamlファイルについては、本アクションの[README](https://github.com/QualityForward/test-stability-report-sync/blob/main/README.md#%E6%9C%89%E5%84%9F%E3%83%86%E3%82%B9%E3%83%88%E3%83%84%E3%83%BC%E3%83%AB)をご確認ください
:::

```.yaml
name: e2e Test and Publish Result

on:
  workflow_dispatch:

jobs:
  RunPytest:
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      # リポジトリをチェックアウト
      - uses: actions/checkout@v4

      - name: Setup Python 3.12
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install poetry
          poetry config virtualenvs.create false
          poetry install
          playwright install --with-deps

      # テスト実行.
      - name: PyTest
        run: |
          python -m pytest tests/ --browser firefox --browser chromium --browser webkit --junit-xml results/pytest.xml
        continue-on-error: true

      # 自動安定性レポート連携アクションを使って、自動テスト結果を連携する
      - name: link automated test results
        uses: QualityForward/test-stability-report-sync@v1.2.0
        with:
          api-key: ${{ secrets.API_KEY }}
          file-path: results/pytest.xml
          test-framework: pytest-playwright
```

<details><summary>自動テスト結果ファイル（xml）</summary>

```xml

<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="3" skipped="0" tests="18" time="105.426" timestamp="2024-09-18T07:57:48.023993" hostname="967691676da3"><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_before_today[firefox]" time="16.172" /><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_no_name[firefox]" time="3.343" /><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_after_90days[firefox]" time="3.170" /><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[firefox-3d-breakfast]" time="4.624" /><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[firefox-2d-early]" time="13.668"><failure message="AssertionError: Locator expected to have text '合計 16,000円（税込み）'&#10;Actual value: 合計 15,000円（税込み） &#10;Call log:&#10;LocatorAssertions.to_have_text with timeout 10000ms&#10;  - waiting for locator(&quot;#total-bill&quot;)&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt; &lt;/h3&gt;&#10;  -   unexpected value &quot; &quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt; &lt;/h3&gt;&#10;  -   unexpected value &quot; &quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;">self = &lt;test_reserve.TestReserve object at 0x7efd4225d790&gt;, page = &lt;Page url='https://hotel.testplanisphere.dev/ja/confirm.html'&gt;, date_from = '2024/10/02', term = 2, breakfast = False, early = True, sightseeing = False
total = '16,000円'

    @pytest.mark.parametrize(
        "date_from,term,breakfast,early,sightseeing,total",
        [
            ("2024/10/01", 3, True, False, False, "24,000円"),
            ("2024/10/02", 2, False, True, False, "16,000円"),
            ("2024/10/03", 1, False, False, True, "8,000円"),
        ],
        ids=["3d-breakfast", "2d-early", "1d-sightseeing"],
    )
    def test_reserve(
        self,
        page: Page,
        date_from: str,
        term: int,
        breakfast: bool,
        early: bool,
        sightseeing: bool,
        total: int,
    ) -&gt; None:
        """宿泊金額が正しいことを確認するテスト"""
        page_reserve = reserve.PageReserve(page)
        page_reserve.navigate()
    
        # 宿泊内容の設定
        page_reserve.set_date_and_term(date_from, term)
        page_reserve.set_headcount(1)
        page_reserve.select_plan(breakfast, early, sightseeing)
        page_reserve.input_name("てすと太郎")
        page_reserve.select_contact("no")
    
        # 予約内容の確認
        page_reserve.confirm()
    
        # 期待値確認
        page_confirm = confirm.PageConfirm(page)
&gt;       expect(page_confirm.total_bill).to_have_text(f"合計 {total}（税込み）", timeout=10000)
E       AssertionError: Locator expected to have text '合計 16,000円（税込み）'
E       Actual value: 合計 15,000円（税込み） 
E       Call log:
E       LocatorAssertions.to_have_text with timeout 10000ms
E         - waiting for locator("#total-bill")
E         -   locator resolved to &lt;h3 id="total-bill"&gt; &lt;/h3&gt;
E         -   unexpected value " "
E         -   locator resolved to &lt;h3 id="total-bill"&gt; &lt;/h3&gt;
E         -   unexpected value " "
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"

tests/test_reserve.py:42: AssertionError</failure></testcase><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[firefox-1d-sightseeing]" time="3.920" /><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_before_today[chromium]" time="9.618" /><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_no_name[chromium]" time="1.675" /><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_after_90days[chromium]" time="2.273" /><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[chromium-3d-breakfast]" time="2.308" /><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[chromium-2d-early]" time="11.072"><failure message="AssertionError: Locator expected to have text '合計 16,000円（税込み）'&#10;Actual value: 合計 15,000円（税込み） &#10;Call log:&#10;LocatorAssertions.to_have_text with timeout 10000ms&#10;  - waiting for locator(&quot;#total-bill&quot;)&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt; &lt;/h3&gt;&#10;  -   unexpected value &quot; &quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;">self = &lt;test_reserve.TestReserve object at 0x7efd4225e180&gt;, page = &lt;Page url='https://hotel.testplanisphere.dev/ja/confirm.html'&gt;, date_from = '2024/10/02', term = 2, breakfast = False, early = True, sightseeing = False
total = '16,000円'

    @pytest.mark.parametrize(
        "date_from,term,breakfast,early,sightseeing,total",
        [
            ("2024/10/01", 3, True, False, False, "24,000円"),
            ("2024/10/02", 2, False, True, False, "16,000円"),
            ("2024/10/03", 1, False, False, True, "8,000円"),
        ],
        ids=["3d-breakfast", "2d-early", "1d-sightseeing"],
    )
    def test_reserve(
        self,
        page: Page,
        date_from: str,
        term: int,
        breakfast: bool,
        early: bool,
        sightseeing: bool,
        total: int,
    ) -&gt; None:
        """宿泊金額が正しいことを確認するテスト"""
        page_reserve = reserve.PageReserve(page)
        page_reserve.navigate()
    
        # 宿泊内容の設定
        page_reserve.set_date_and_term(date_from, term)
        page_reserve.set_headcount(1)
        page_reserve.select_plan(breakfast, early, sightseeing)
        page_reserve.input_name("てすと太郎")
        page_reserve.select_contact("no")
    
        # 予約内容の確認
        page_reserve.confirm()
    
        # 期待値確認
        page_confirm = confirm.PageConfirm(page)
&gt;       expect(page_confirm.total_bill).to_have_text(f"合計 {total}（税込み）", timeout=10000)
E       AssertionError: Locator expected to have text '合計 16,000円（税込み）'
E       Actual value: 合計 15,000円（税込み） 
E       Call log:
E       LocatorAssertions.to_have_text with timeout 10000ms
E         - waiting for locator("#total-bill")
E         -   locator resolved to &lt;h3 id="total-bill"&gt; &lt;/h3&gt;
E         -   unexpected value " "
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"

tests/test_reserve.py:42: AssertionError</failure></testcase><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[chromium-1d-sightseeing]" time="3.350" /><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_before_today[webkit]" time="7.449" /><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_no_name[webkit]" time="2.115" /><testcase classname="tests.test_invalid_reserve.TestInvalidReserve" name="test_after_90days[webkit]" time="1.547" /><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[webkit-3d-breakfast]" time="2.161" /><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[webkit-2d-early]" time="11.799"><failure message="AssertionError: Locator expected to have text '合計 16,000円（税込み）'&#10;Actual value: 合計 15,000円（税込み） &#10;Call log:&#10;LocatorAssertions.to_have_text with timeout 10000ms&#10;  - waiting for locator(&quot;#total-bill&quot;)&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt; &lt;/h3&gt;&#10;  -   unexpected value &quot; &quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt; &lt;/h3&gt;&#10;  -   unexpected value &quot; &quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;&#10;  -   locator resolved to &lt;h3 id=&quot;total-bill&quot;&gt;合計 15,000円（税込み）&lt;/h3&gt;&#10;  -   unexpected value &quot;合計 15,000円（税込み）&quot;">self = &lt;test_reserve.TestReserve object at 0x7efd4225e330&gt;, page = &lt;Page url='https://hotel.testplanisphere.dev/ja/confirm.html'&gt;, date_from = '2024/10/02', term = 2, breakfast = False, early = True, sightseeing = False
total = '16,000円'

    @pytest.mark.parametrize(
        "date_from,term,breakfast,early,sightseeing,total",
        [
            ("2024/10/01", 3, True, False, False, "24,000円"),
            ("2024/10/02", 2, False, True, False, "16,000円"),
            ("2024/10/03", 1, False, False, True, "8,000円"),
        ],
        ids=["3d-breakfast", "2d-early", "1d-sightseeing"],
    )
    def test_reserve(
        self,
        page: Page,
        date_from: str,
        term: int,
        breakfast: bool,
        early: bool,
        sightseeing: bool,
        total: int,
    ) -&gt; None:
        """宿泊金額が正しいことを確認するテスト"""
        page_reserve = reserve.PageReserve(page)
        page_reserve.navigate()
    
        # 宿泊内容の設定
        page_reserve.set_date_and_term(date_from, term)
        page_reserve.set_headcount(1)
        page_reserve.select_plan(breakfast, early, sightseeing)
        page_reserve.input_name("てすと太郎")
        page_reserve.select_contact("no")
    
        # 予約内容の確認
        page_reserve.confirm()
    
        # 期待値確認
        page_confirm = confirm.PageConfirm(page)
&gt;       expect(page_confirm.total_bill).to_have_text(f"合計 {total}（税込み）", timeout=10000)
E       AssertionError: Locator expected to have text '合計 16,000円（税込み）'
E       Actual value: 合計 15,000円（税込み） 
E       Call log:
E       LocatorAssertions.to_have_text with timeout 10000ms
E         - waiting for locator("#total-bill")
E         -   locator resolved to &lt;h3 id="total-bill"&gt; &lt;/h3&gt;
E         -   unexpected value " "
E         -   locator resolved to &lt;h3 id="total-bill"&gt; &lt;/h3&gt;
E         -   unexpected value " "
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"
E         -   locator resolved to &lt;h3 id="total-bill"&gt;合計 15,000円（税込み）&lt;/h3&gt;
E         -   unexpected value "合計 15,000円（税込み）"

tests/test_reserve.py:42: AssertionError</failure></testcase><testcase classname="tests.test_reserve.TestReserve" name="test_reserve[webkit-1d-sightseeing]" time="4.966" /></testsuite></testsuites>

```

</details>

では、本アクションの設定項目を説明します。

#### 自動安定性レポート連携アクションの設定項目（必須項目）

**`api-key`**

Quality ForwardのプロジェクトAPIキー[^9]を設定してください。
※[GitHub Actionsでのシークレットの使用](https://docs.github.com/ja/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions)を推奨

**`file-path`**

自動テストの実行結果を格納したファイルパスを設定してください。
※ワイルドカードで指定可能です
※テストフレームワークによって、有効なファイル拡張子が異なります

**`test-framework`**

使用したテストフレームワークを設定してください。

設定できるテストフレームワーク

|テストフレームワーク|出力ファイル拡張子|備考|
|:--:|:--:|:--|
|junit|xml|データパターン未対応|
|pytest|xml|-|
|nodejs-playwright|xml|データパターン未対応|
|pytest-playwright|xml|-|
|magicpod|json|-|
|mabl-deploy-event[^10]|json|-|

:::note info
上記以外のテストフレームワークでも、junit-xml形式のファイルで出力できるものは**junit**を選択してください
:::

#### 自動安定性レポート連携アクションの設定項目（任意項目）

**`auto_test_suite_external_key`**

連携する自動テストスイート名を設定してください。

設定しない場合の自動テストスイート名

|テストフレームワーク|自動テストスイート名|
|:--:|:--|
|junit,nodejs-playwright|testsuite要素のname属性|
|pytest,pytest-playwright|testcase要素のclassname属性<br>最後のドット以降を取得<br>（例）test.testSuiteの場合、”testSuite”を取得|
|magicpod|テスト一括実行設定名|
|mabl-deploy-event|テストプランid|

**`auto_test_cycle_name`**

連携する自動テストサイクル名を設定してください。

設定しない場合の自動テストサイクル名

|テストフレームワーク|自動テストサイクル名|
|:--:|:--|
|junit,mabl-deploy-event|アクション実行日時|
|pytest,nodejs-playwright,pytest-playwright|testsuite要素のtimestamp属性|
|magicpod|テスト一括実行終了日時|

**`auto_execution_device_external_key`**

連携する自動テスト実施環境名を設定してください。

設定しない場合の自動テスト実施環境名

|テストフレームワーク|自動テスト実施環境名|
|:--:|:--|
|junit,pytest,magicpod|`標準環境`と表示|
|nodejs-playwright|testsuite要素のhostname属性|
|pytest-playwright|testcase要素のname属性から取得<br>（例）test_sample[chromium-data-driven]の場合、`chromium`を取得|
|mabl-deploy-event|各 journey_executions の browser_type 属性<br>browser_type 属性がない場合は`標準環境`|

#### 実施後の表示

yamlファイル作成後、GitHubActionsのワークフローで実行します。
成功するとコンソールに「The integration of automated test results has been completed.」が表示されます。

![GitHubActions結果.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1895890/6f5b3d03-ced8-b1b7-ee55-c51cca79820d.png)

QualityForwardの自動テスト安定性レポートを確認すると投入した自動テスト結果を確認できます。

<自動テスト全体>

![連携結果.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1895890/5d192649-25d1-e2f6-37b0-b277acbd94d3.png)

<自動テストスイート詳細>

![連携後の自動テストスイート詳細.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1895890/e889180a-8495-115e-630c-962b611979c8.png)

:::note info
自動テストスイート名や自動テストケース名等をカスタマイズしたい場合、独自のデータ整形スクリプトを作成することを推奨します。
:::

## おわりに

今回はQualityForwardの自動テスト安定性レポートの概要と簡単に活用できるGitHub Actionsについて説明しました。

GitHub Actionsでもまだまだ対応しきれていないフレームワークがあるため、これからも対応フレームワークを増やしていく予定です。
自動テスト結果分析の時間を短くしたい、QualityForwardの自動テスト安定性レポートの表示内容をとりあえず見てみたい方は、ぜひこのアクションを試してみてください。

https://github.com/QualityForward/test-stability-report-sync

この記事が少しでもお役に立てば幸いです。
ご意見やご質問がございましたら、ぜひフィードバックをお寄せください。

## 参考資料

- [QualityForward](https://www.veriserve.co.jp/helloqualityworld/service/qualityforward/)
- [テスト管理ツール「QualityForward」がテスト自動化をサポートする新機能を提供開始](https://www.veriserve.co.jp/news/2024/news-20240704.html)
- [QualityForwardの基本用語](https://qualityforward.zendesk.com/hc/ja/articles/5049964535577-%E5%9F%BA%E6%9C%AC%E7%94%A8%E8%AA%9E)
- [QualityForward API](https://qualityforward.github.io/api-spec/#tag/AutoTestResult)
- [ソフトウェアテストをカイゼンする50のアイデア](https://www.amazon.co.jp/dp/B0B773S5CP)

[^1]:QualityForwardはテストの資産管理、実行管理、結果管理のためのテスト管理ツールです。テスト実行の進捗状況や、計画に対する予実、検出されたバグなどを自動的に集計し可視化します。これにより、テスト実行者とテスト管理者のやり取りや集計を効率化できます。詳細は[QualityForwardの製品ページ](https://www.veriserve.co.jp/helloqualityworld/service/qualityforward/)をご確認ください。

[^2]:[QualityForwardのテスト自動化支援機能について](https://qualityforward.zendesk.com/hc/article_attachments/34661244680857)

[^3]:ほかには機能カバレッジの拡大、総テストコストの削減、手動テスト担当者が行えないテストの実施、テスト実行期間の短縮やテスト実行頻度の向上およびテストサイクルに要する時間の短縮などがあります。（[JSTQB ALシラバス（Specialist）テスト自動化エンジニア Version 2016.J01](https://www.jstqb.jp/syllabus.html#syllabus_advanced_specialist)より）

[^4]:断続的に失敗するテストのこと。この原因はテストケース、テスト対象、テストフレームワーク等いろんな要因があるため、調査や修正に時間がかかるため、改善（テストの削除やスクリプトの修正）が必要になる。

[^5]:QualityForwardではソフトウェアテストの目的や対象ごとに複数のテストケースをまとめたものと定義しています。たとえばテストケースをExcelで1シートずつに分けて管理している場合、以下の図のようにそれらを1つ1つのテストスイートに分けていくイメージです。[基本用語 – QualityForwardサポートサイト](https://qualityforward.zendesk.com/hc/ja/articles/5049964535577-%E5%9F%BA%E6%9C%AC%E7%94%A8%E8%AA%9E)のテストスイートより

[^6]:webの場合、chrome、Edge、firefoxなどがあります。

[^7]:データ駆動で用いられるデータのこと

[^8]:テストを実施する最小単位の期間です。1つのテストスイートに対して「テスト開始日」と「テスト終了日」を定めることで、1つのテストサイクルになります。[基本用語 – QualityForwardサポートサイト](https://qualityforward.zendesk.com/hc/ja/articles/5049964535577-%E5%9F%BA%E6%9C%AC%E7%94%A8%E8%AA%9E)のテストサイクルより

[^9]:APIキーの生成はQualityForwardの[APIキーの発行手順](https://qualityforward.github.io/api-spec/#section/API/API)をご確認ください。

[^10]:[デプロイ結果のサマリー取得](https://api.help.mabl.com/reference/getexecutionresultforevent)のデータ（詳細は mabl ヘルプページ - [デプロイイベント](https://help.mabl.com/hc/ja/articles/17780788992148-%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88)を参照のこと）