AGEST Engineers Blog

株式会社AGESTのテックブログです。

TestRailと自動テストの連携① Selenium/Python編

はじめまして。 テストオートメーショングループの佐々木です。

先日開催されたJaSST'22 Tokyoにて、TestRailの紹介セッションを拝見しました。
便利そうだなぁ。きれいなレポートいいなぁ。オリジナリティ溢れるエクセル項目書はもう触りたくないなぁ。

と、つぶやいていた私にTestRailの調査依頼が来ました。
自分の意見を言っておくのは大事ですね。

上司「うちの部で使っている何かと、TestRailを連携して何かいい感じのことやって」
なんという自由度の高い依頼でしょう。すばらしい。

今回はTestRailとSelenium/Pythonの自動テストを連携したお話です。
連携方法やテストコードは下の方で公開しています。
(後で見たら「このコードを書いたのは誰だぁ!!(自分)」となるのですが、あえて公開します。)

連携図

自動化した内容

今回の連携で自動化したのは以下の3点です。

  1. テストが実行されるたびにTestRailにテスト結果を記入する。

  2. テストが失敗した場合はGithubにissueを作成する。
    ※「29 days ago」になっていますが、テスト実施時に自動起票されています。

  3. 失敗したテストにテスト失敗時のスクリーンショットを添付し、「欠陥」にissueへのリンクを設定する。

連携手順①GithubとTestRailの設定

Githubの設定

※ユーザの登録やリポジトリの作成方法までは、ほかのサイトなどを参考にしてください。

今回はBTSとしてGithubを利用しましたが、TestRailには公式の連携先がたくさんあります。連携方法も記載されているので、ぜひこちらで探してみてください。

https://docs.testrail.techmatrix.jp/testrail/docs/integrate/

Githubのアクセストークンを発行する

  1. Githubのヘッダー右端のユーザーアイコンから「Settings」を押します
  2. 左メニューの一番下にある「Developer settings」を押します
  3. 左メニューの「Personal access tokens」を押します
  4. 「Generate new token」を押します
  5. 「Note」には何のためのトークンかわかるように名前を付けます
    例:TestRail接続用
  6. 「Expiration」はトークンの有効期限です。使用目的に合わせた適切な日数を入れます
  7. 「Select scopes」は権限の範囲です。今回はTestRailでissueを見るだけなので、どこにもチェックは入れません
  8. 「Generate token」を押します
  9. トークンをコピーします。このキーは二度と表示されないので、確実にコピーして保存しておきます

TestRailの設定

※プロジェクトを作成するところまでは、ほかのサイトなどを参考にしてください。

TestRailにユーザー変数を追加する(Github連携)

  1. ダッシュボードの右端にある「管理」を押します
  2. 「管理」カテゴリにある「プロジェクト」を押します
  3. 連携するプロジェクトを押します
  4. タブの「ユーザー変数」を押します
  5. 「ユーザー変数の追加」を押します
  6. まずはGithubのIDを入れる変数を作成します。
    ① 「ラベル」:GithubのログインIDなどわかるような名前にします
    ②「システム名」:github_userなどわかるような名前にします
    ③「タイプ」:「文字列」にします
    ④「フォールバック」:空欄にします
    ⑤ 「OK」を押します

  7. 次にGithubのパスワードを入れる変数を作成します。手順は上と同じですが、「タイプ」をパスワードにします。内容が●でマスクされるようになります。

TestRailでユーザー変数を設定する(Github連携)

  1. 右上にあるユーザー名から「個人設定」を押します
  2. 「設定」タブを開きます
  3. 前の手順で作成した変数に、GithubにログインするためのIDとパスワードを入力します
  4. 「✔設定の保存」を押します

TestRailのプロジェクトに参照を設定する(Github連携)

  1. TestRailで、連携するプロジェクトを開きます
  2. プロジェクト画面の右上にある「✐編集」を押します
  3. 「欠陥」タブを開きます
  4. 「欠陥表示 URL」に、リポジトリのissueページのURLを入れます
https://github.com/ユーザ名/リポジトリ名/issues/ケースIDのプレースホルダ

具体例: https://github.com/hoge/TestSelenium/issues/%id%※%id%のところがケースIDのプレースホルダです。ここに数字が入ってリンクになります

 5. 「欠陥追加 URL」に新規起票用のURLを入れます

https://github.com/ユーザ名/リポジトリ名/issues/new

具体例: https://github.com/hoge/TestSelenium/issues/new
 6. 「欠陥プラグイン」からGithubを選択します

 7. 自動入力されたプラグインで、以下の赤文字の部分を入力します

[connection]
address=https://api.github.com/
user=**%github_user%          //設定したユーザIDの変数名**
password=**%github_password%      //設定したパスワードの変数名**

[repository]
owner=**hoge                  //リポジトリのオーナー名**
name=**TestRailSelenium            //リポジトリ名**

[push.fields]
summary=on
milestone=on
assignee=on
label=on
description=on

[hover.fields]
summary=on
milestone=on
assignee=on
label=on
description=on


 8. 「✔プロジェクトの保存」を押します

TestRailでAPIを許可する

  1. ダッシュボードの右端にある「管理」を押します
  2. 「管理」カテゴリにある「サイト設定」を押します
  3. 「API」タブを開きます
  4. 「API の有効化」にチェックを入れます

連携手順②コードの実装

「ソフトウェア・テストの技法」よりこちらのページを使用させていただきました。

プロジェクトフォルダの構成

こちらがプロジェクトフォルダの構成です。上から順番に説明していきます。

Config/config.py

TestRailとGithubの接続に必要なトークンやログイン情報を記載します。

## Githubの設定
token_github = "Githubのトークンを入れます"
base_url_github = "GithubのレポジトリのURLを入れます"

## TestRailの設定
user_testrail = "TestRailのユーザーIDを入れます"
password_testrail = "TestRailのパスワードを入れます"
base_url_testrail = "TestRailのURLを入れます(index.php?の前の部分まで)"

Data/test_data.csv

テスト用のデータです。以下のようなCSVファイルを使用しています。

※Githubとの連携を確認するため、いくつか失敗するデータを入れておきます。

 No  A B C enterd_A enterd_B enterd_C result objectives testID
1 1 1 1 1 1 1 正三角形 正三角形のテスト
2 10 10 5 10 10 5 二等辺三角形 二等辺三角形のテスト(ABが等しい)
3 10 5 10 10 5 10 失敗するテスト 二等辺三角形のテスト
(中略)
42 5 5 Null 5 5 入力は1~99999の整数のみです CがNullのテスト
  • 各カラムの内容は
  • No:テスト番号
  • A,B,C:各辺に入れる値
  • enterd_A,enterd_B,enterd_C:テキストボックスに値を入力した時の期待結果
  • result:「表示」ボタンを押した後の期待結果
  • objectives:テストの目的
  • testID:TestRailのテストID。インポート後に記入します

このCSVファイルをTestRailにインポートしてテストを作成します。
読み込ませた後は、TestRailのここの数字をCSVファイルの「testID」に入れておきます。

このIDを基準に自動処理を行います。

Module/github.py

GithubのAPIを利用するためのファイルです。
今回は以下の二つのAPIを使用します

  • issueを作成するAPI
  • 作成したissueのIDを取得するAPI
import json
import requests

import Config.config as config


class GithubAPIClient:
    def __init__(self):
        self.GITHUB_TOKEN = config.token_github
        self.base_url = config.base_url_github

    def create_issue(self, title):
        """Githubにissueを作成する"""
        uri = f"/issues"
        url = self.base_url + uri
        headers = {}
        headers['Authorization'] = 'Bearer ' + self.GITHUB_TOKEN
        headers['Content-Type'] = 'application/json'
        headers['Accept'] = 'application/vnd.github.v3+json'
        data = {}
        data["title"] = title
        # issueの本文
        data["body"] = "これは自動作成されたissueです。"
        payload = bytes(json.dumps(data), 'utf-8')
        response = requests.post(url, headers=headers, data=payload)
        return response

    def get_issues(self):
        """issueのデータを取得する"""
        uri = f"/issues"
        url = self.base_url + uri
        headers = {}
        headers['Authorization'] = 'Bearer ' + self.GITHUB_TOKEN
        headers['Accept'] = 'application/vnd.github.v3+json'
        response = requests.get(url, headers=headers)
        return response.json()

Module/testrail.py

TestRailのAPIを利用するためのファイルです。
今回は以下のAPIを使用します。

  • テスト結果を入力するAPI
  • テスト結果に画像データを添付するAPI

TestRailはAPI組み込み用のパーツを公開しています。
https://docs.testrail.techmatrix.jp/testrail/docs/api/getting-started/
こちらを利用すると連携が容易です。

import base64
import json
import requests

import Config.config as config


class APIClient:
   def __init__(self):
        self.user = config.user_testrail
        self.password = config.password_testrail
        self.base_url = config.base_url_testrail
        if not self.base_url.endswith('/'):
            self.base_url += '/'
        self.__url = self.base_url + 'index.php?/api/v2/'

    def send_get(self, uri, filepath=None):
    # 中略。TestRailのパーツを利用

    def post_test_result_pass(self, testID):
        """TestRailにテスト結果PassedをPOSTする"""
        uri = f"add_result/{testID}"
        # TestRailでPassの結果は「1」
        data = {"status_id": 1,
                "comment": "テスト成功。Pythonでadd_resultAPIにより入力",
                "version": "バージョン0.1",
                "elapsed": "",
                "defects": "",
                "assignedto_id": ""
                }
        self.send_post(uri, data)

    def attach_data_to_test_result(self, testID, path):
        """TestRailのテスト結果にスクショをアップロードする"""
        # オプション「&limit=1」をつけて最新のテスト結果を取得します
        uri = f"get_results/{testID}&limit=1"
        result = self.send_get(uri)
        # テスト結果IDを取り出します
        test_id = result["results"][0]["id"]
        # 取得した結果IDに画像ファイルをアップロードします
        uri = f"add_attachment_to_result/{test_id}"
        self.send_post(uri, path)

    def post_test_result_fail(self, testID, errorlog, defectID):
        """TestRailにテスト結果FailedをPOSTする"""
        uri = f"add_result/{testID}"
        # TestRailでFailの結果は「5」
        data = {"status_id": 5,
                "version": "バージョン0.1",
                "elapsed": "",
                "assignedto_id": ""
                }
        data["comment"] = errorlog
        data["defects"] = defectID
        self.send_post(uri, data)

class APIError(Exception):
    pass

Pages/BasePage.py

操作の基本となるパーツを記載したファイルです。
今回はAPIがメインなので、詳しい説明は省略します。

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(self.driver, 10)

    def click_element(self, locator):
        self.wait.until(EC.visibility_of_element_located(locator)).click()

    def send_keys(self, locator, text):
        self.wait.until(EC.visibility_of_element_located(
            locator)).send_keys(text)

    def get_element_text(self, locator):
        element = self.wait.until(EC.visibility_of_element_located(locator))
        return element.text

    def get_element_value(self, locator):
        element = self.wait.until(EC.visibility_of_element_located(locator))
        return element.get_attribute("value")

    def take_screenshot(self, name):
        self.driver.save_screenshot(name)

Pages/Sankaku.py

テスト対象ページの操作パーツを記載したファイルです。
こちらも詳しい説明は省略します。

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from Pages.BasePage import BasePage


class MainPage(BasePage):

    # ページ情報定義
    URL = "http://milk0824.sakura.ne.jp/services/myers/"

    # ロケータ定義
    CONTENT_TOP = (By.XPATH, '//*[@id="MAIN_WND"]/h1')
    HEN_A = (By.XPATH, '//*[@id="SIDE_A"]')
    HEN_B = (By.XPATH, '//*[@id="SIDE_B"]')
    HEN_C = (By.XPATH, '//*[@id="SIDE_C"]')
    HYOUJI_BUTTON = (By.XPATH, '//*[@id="MAIN_WND"]/div[2]/center/button')
    KEKKA_TEXT = (By.XPATH, '//*[@id="DISPLAY"]')

    def __init__(self, driver):
        super().__init__(driver)

    def _wait_until_page_displayed(self):
        self.wait.until(EC.visibility_of_element_located(self.HEN_A))

    def input_text(self, place, text):
        if place == "A":
            locator = self.HEN_A
        elif place == "B":
            locator = self.HEN_B
        elif place == "C":
            locator = self.HEN_C
        self.send_keys(locator, text)

    def click_hyouji_button(self):
        self.click_element(self.HYOUJI_BUTTON)

Screenshots

スクリーンショットを格納するフォルダです。

Tests/conftest.py

ドライバの生成や、テストパラメータの読み込みを行うファイルです。
こちらも詳しい説明は省略します。そのうち別の記事にします。

import csv
import pytest

from selenium import webdriver
from selenium.webdriver.chrome import service
from webdriver_manager.chrome import ChromeDriverManager


@pytest.fixture(scope='class')
def init_driver(request):
    chrome_service = service.Service(executable_path=ChromeDriverManager().install())
    chrome_options = webdriver.ChromeOptions()
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--incognito")
    web_driver = webdriver.Chrome(service=chrome_service, options=chrome_options)
    web_driver.set_window_size("1200", "900")
    request.cls.driver = web_driver
    yield
    # 事後処理
    web_driver.quit()

def load_csv():
    with open(r"../Data/test_data.csv", encoding='utf-8') as test_data_file:
        data = csv.DictReader(test_data_file)
        read_data = [row for row in data]
        test_data = []
        for data in read_data:
            num = data["No"]
            value_A = data["A"]
            value_B = data["B"]
            value_C = data["C"]
            result = data["result"]
            objectives = data["objectives"]
            testID = data["testID"]
            t = (num, value_A, value_B, value_C, result, objectives, testID)
            test_data.append(t)
        return test_data

Tests/test_Sankaku.py

テスト内容を記載したファイルです。

import pytest
import sys

from Pages.mainpage import MainPage
from Module.testrail import APIClient
from Module.github import  GithubAPIClient
from Tests.conftest import load_csv


@pytest.mark.usefixtures("init_driver", "make_screenshot_directry", "send_mail")
class Test_Sankaku():
    """マイヤーズの三角形問題テストクラス"""
    test_data = load_csv()
    screenshot_directry = "../Screenshots"

    @pytest.fixture(autouse=True)
    def classObjects(self):
        self.MainPage = MainPage(self.driver)
        self.APIClient = APIClient()
        self.GithubAPIClient = GithubAPIClient()

    @pytest.mark.parametrize("num, value_A, value_B, value_C, result, objectives, testID", test_data)
    def test_method_csv(self, num, value_A, value_B, value_C, result, objectives, testID):
        """テストケース実行"""
        print(f"マイヤーズの三角形問題 - テスト{num}開始 目的 : {objectives}")
        self.MainPage.driver.get(self.MainPage.URL)
        self.MainPage.input_text("A", value_A)
        self.MainPage.input_text("B", value_B)
        self.MainPage.input_text("C", value_C)
        self.MainPage.click_hyouji_button()

        screenshot_path = str(self.screenshot_directry + "\\" + sys._getframe().f_code.co_name + str(num) + '.png')
        self.MainPage.take_screenshot(screenshot_path)

        # TestRail、Githubと連携する処理
        if self.MainPage.get_element_text(self.MainPage.KEKKA_TEXT) == result:
            self.APIClient.post_test_result_Pass(testID)
            assert True
        else:
            errorlog = "三角形の判定と期待結果が一致しません"
            # Githubにissueを作成する処理
            self.GithubAPIClient.create_issue(errorlog)
            # 作成したissueの番号を取得する
            issue_data = self.GithubAPIClient.get_issues()
            # 作成したすぐ後に取得するので、[0]が目的のデータ
            issue_num_int = issue_data[0]["number"]
            issue_num = str(issue_num_int)
            # TestRailにテスト結果と、Githubで作成したissueの番号を記載する
            self.APIClient.post_test_result_Fail(testID, errorlog, issue_num)
            # 失敗したテストのスクショをTestRailにアップする
            self.APIClient.attach_data_to_test_result(testID, screenshot_path)
            assert False
        print(f"マイヤーズの三角形問題 - テスト{num}終了")

実行手順

test_Sankaku.pyの階層に移動したあと、’pytest’コマンドで実行します。
TestRailやGithubのページを更新すると、自動的に結果の入力やissueの作成がされていることを確認できます。

まとめ

今回は基本部分のみの紹介でしたが、他のAPIと組み合わせるとより便利になります。
テストが終わったらレポートを添付したメールを送信したり、テストログや画像をissueに載せたりすることも可能です。
面倒な操作は自動化して、どんどんテストを楽にしましょう。

■ AGESTは一緒に働くメンバーを募集しています! hrmos.co

  • f:id:zo_03:20211213095237p:plain
  • f:id:zo_03:20211213095237p:plain
©AGEST, Inc.