CircleCIでSeleniumのテストを定期実行する 

## 概要

本章では、CircleCIというCIツールを使って、Python+Seleniumで作成した自動テストを定期的に実行する方法を解説します。

公式のドキュメント（[CircleCIのドキュメントへようこそ - CircleCI](https://circleci.com/docs/ja/)）を参考にしつつ、やってみてください。

## 用意するもの

- GitHubのアカウント
  - [【2019年1月現在】GitHubアカウント作成方法 - Qiita](https://qiita.com/okumurakengo/items/848f7177765cf25fcde0)　を参照
- CircleCIのアカウント
  - [CircleCI を始める - CircleCI](https://circleci.com/docs/ja/2.0/first-steps/)　を参照
  - GitHubアカウントでログインすると、連携が簡単です
- Selenium + Python + pytestで作成した自動テスト
  - 別途サンプルを用意しておくので、無い場合は無いでも大丈夫です。ご自身で動かしたい自動テストがある方は、ソースコード一式と、テストを動かすためにpipで入れるものリストを用意しておいてください。

<div style="page-break-before:always"></div>

## CircleCIでSelenium+Pythonのコードを実行する

### 1. GitHubにリポジトリをつくる

最初に、GitHubにリポジトリを作ります。ここでは[YoshikiIto/Selenium-circleci](https://github.com/YoshikiIto/Selenium-circleci)を作成した、として進めます。

![CircleCI_002.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/d7b9f808-12e9-904d-0a5c-9e36e3db96d7.png)


### 2. コードを追加

動かしたいテストがある方はそのファイルを。ない方は下記のサンプルをお使いください。

```
Selenium-circleci
 |- test_google.py <- 新規追加
```

`test_google.py`というファイル名で、以下のコードをリポジトリ内に作成します。

``` Python
import pytest
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

class TestGoogle:

    driver = webdriver.Chrome(ChromeDriverManager().install())

    def setup_class(cls):
        cls.driver = webdriver.Chrome(ChromeDriverManager().install())
        cls.driver.maximize_window()

    def setup_method(self):
        self.driver.get("https://google.com")

    def test_case(self):
        driver = self.driver

        driver.save_screenshot("test-reports/result_001.png")

        driver.find_element_by_css_selector("#tsf > div:nth-child(2) > div > div.RNNXgb > div > div.a4bIc > input").send_keys("hoge")

        driver.save_screenshot("test-reports/result_002.png")

        driver.find_element_by_css_selector("#tsf > div:nth-child(2) > div > div.FPdoLc.VlcLAe > center > input.gNO89b").click()

        driver.save_screenshot("test-reports/result_003.png")

        assert driver.title.count('hoge'), "ページタイトルにhogeが含まれていること"

    def teardown_class(cls):
        cls.driver.quit()

```

上記のコードを正しく実行できた場合、以下のような動作をします。

1. Googleのトップページを開く
2. スクリーンショットを撮る
3. 検索バーに「hoge」と入力する
4. スクリーンショットを撮る
5. Google検索ボタンを押す
6. ページ遷移後のスクリーンショットを撮る

なお、上記のコードでは[SergeyPirogov/webdriver_manager](https://github.com/SergeyPirogov/webdriver_manager)を使用しているため、ローカルで動かす場合には以下のコマンドでインストールしてから実行してください。

``` Bash
$ pip install webdriver_manager
```

### 3. pipでインストールするパッケージの一覧を作成

```
Selenium-circleci
 |- test_google.py
 |- requirements.txt <- 新規追加
```

pytestや、前項のwebdriver_managerなど、実行前にpipでインストールする必要があるものをまとめた`requirements.txt`を作成します。

※これはCircleCIの機能というわけではなく、複数のパッケージを一括インストールするためのpip側の仕組みです。参考：[Python, pipでrequirements.txtを使ってパッケージ一括インストール | note.nkmk.me](https://note.nkmk.me/python-pip-install-requirements/)

`requirements.txt`の内容は以下です。

```
pytest
selenium
webdriver_manager
```

ご自身で用意したテストを動かす場合は、別途使っているパッケージも`requirements.txt`の中に書いておいてください。

### 4. 設定ファイルを追加

```
Selenium-circleci
 |- test_google.py
 |- requirements.txt
 |- .circleci
      |- config.yml <- 新規追加
```

CircleCIで実行したい処理の詳細は、`.circleci/config.yml`という設定ファイルに記載していきます。

※CircleCIのバージョンによってconfigの場所が違うので、ネット上の情報を見るときにはどのバージョンの話なのかを気にするようにしてください。

詳細な解説は本ドキュメントの後半で行うので、一旦はこの通りでやってみましょう。

``` YAML
# Python CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-python/ for more details
#
version: 2
jobs:
  build:
    docker:
      # specify the version you desire here
      # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers`
      - image: circleci/python:3.7.3-browsers

      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/postgres:9.4

    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "requirements.txt" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run:
          name: install dependencies
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install --upgrade pip
            pip install -r requirements.txt

      - run: mkdir test-reports

      - save_cache:
          paths:
            - ./venv
          key: v1-dependencies-{{ checksum "requirements.txt" }}

      # run tests!
      # this example uses Django's built-in test-runner
      # other common Python testing frameworks include pytest and nose
      # https://pytest.org
      # https://nose.readthedocs.io
      - run:
          name: run tests
          command: |
            . venv/bin/activate
            pytest test_google.py -v --junit-xml=test-results/regression/results.xml

      - store_artifacts:
          path: test-reports/
          destination: test-reports

      - store_test_results:
          path: test-results

workflows:
  version: 2
  normal_workflow:
    jobs:
      - build
  schedule_workflow:
    triggers:
      - schedule:
          cron: "1 7 * * *" # UTCで記述。-9
          filters:
            branches:
              only:
                - master
    jobs:
      - build
```

ここまでできたら、GitHubのリモートリポジトリにすべてプッシュしておいてください。

### 4. CircleCI上でSet Up Project

CircleCiにログインします。

![CircleCI_001.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/d7435835-ea0a-a433-3759-3eac18d3b1c7.png)


ここに最初のプロジェクトの始め方が書いてあります。さっと読んで、「Start a Project」ボタンをクリックします。

![CircleCI_003.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/5a991395-bb2d-6eba-239c-0275dfdf5f71.png)


GitHub上のリポジトリが表示されるので、この中から今回のテスト用に作成したリポジトリ（ここではSelenium-circleci）の「Set Up Project」ボタンをクリックします。

![CircleCI_004.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/ba9e8fed-b7d0-8a7c-85d3-995b7cb174eb.png)

Operating SystemではLinux、LanguageではPythonを選びましょう。

「Copy to Clipboard」のところは、すでに`config.yml`ファイル作成済みのため、行わなくて大丈夫です。

### 5. CircleCI上でビルド

CircleCIでジョブの実行を行うことを「ビルドする」と言います（プログラムのソースコードのビルドと混同しやすいので注意してください）。

水色の「Start building」ボタンをクリックしましょう。ビルドが終わると結果が表示されます。

ビルドが正しく実行された場合の例が以下です。

![CircleCI_005.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/2d34503c-dac8-cd13-907e-8953948d886c.png)

`test_google.py`にわざと失敗するassertを追加してみましょう。

``` Python
assert driver.title.count('hoge'), "ページタイトルにhogeが含まれていること"
assert driver.title.count('foobar'), "ページタイトルにfoobarが含まれていること"  # わざと失敗させる
```

この状態でビルドを実行し、失敗した状態の例が以下です。

![CircleCI_006.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/bf75ddc2-7c39-7153-3ed8-e1b38741e865.png)


<div style="page-break-before:always"></div>

## テストレポートを出力する

ここまでの手順では、テスト実行の結果がビルドのPass/Failとしてしか表示されていませんでした。しかし、実際にテストを行う場合、何ケース実行してそのうち何ケースがPassしたのかなどのレポートが欲しくなります。

簡単に使えるものとして、[pytest-html](https://github.com/pytest-dev/pytest-html)を使って、レポート表示をしてみましょう。

出力したレポートのサンプルが以下です。

![CircleCI_007.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/45d7c556-e213-324e-7c54-0817144c374d.png)


### 1. requirements.txtの編集

`pytest-html`が必要なので、pipでインストールできるよう`requirements.txt`を以下のように編集しましょう。

```
pytest
selenium
webdriver_manager
pytest-html
```

### 2. config.ymlの編集

`config.yml`を編集して、実際にpytestコマンドでテストを行う際にHTMLレポートも出力するようにオプションを追加します。

以下、編集後の`config.yml`の抜粋です。

``` Yaml
      - run:
          name: run tests
          command: |
            . venv/bin/activate
            pytest test_google.py -v --junit-xml=test-results/regression/results.xml --html=test-reports/report.html --self-contained-html
```

### 3. ビルド

ここまでできたら、CircleCIでビルドをします。

ビルドの結果、今回の設定ではArtifacts内に`report.html`が出力されています。

![CircleCI_008.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46492/0b722dd4-8933-d4b1-ef35-c99bf7f9a000.png)


<div style="page-break-before:always"></div>

## `config.yml`の解説

### Dockerイメージの指定

CircleCIで自動テストを動かす際は、使っている言語ごとに用意されているDockerイメージを指定します。

言語のバージョンも細かく選ぶことができ、一覧は[Pre-Built CircleCI Docker Images - CircleCI](https://circleci.com/docs/ja/2.0/circleci-images/)にあります。

Seleniumの自動テストを実行するにあたって気を付ける点は、末尾に`-browsers`をつけることです。

``` Yaml
    docker:
      # specify the version you desire here
      # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers`
      - image: circleci/python:3.7.3-browsers
```

CircleCI公式の`config.yml`のコメントにも同様の内容が書いてあります（prefixじゃなくてpostfixでは？　という気もしますが）

### stepsの記述

基本的に、実行したい処理のまとまりごとにrunコマンドを設定していきます。

pipを使ったモジュールのインストールが以下です。

``` Yaml
      - run:
          name: install dependencies
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install --upgrade pip
            pip install -r requirements.txt
```

都度pipのupgradeと、各種モジュールのインストール（実際には最新版の有無確認）が走るようにしています。それほど時間のロスにはなっていません。

次に、テスト実行結果の格納用に`mkdir`コマンドを実行しています。

``` Yaml
      - run: mkdir test-reports
```

テスト実行コマンドがこちらです。

``` Yaml
      - run:
          name: run tests
          command: |
            . venv/bin/activate
            pytest test_google.py -v --junit-xml=test-results/regression/results.xml --html=test-reports/report.html --self-contained-html
```

pytestの実行コマンドに、追加でxmlのレポートとhtmlのレポートを出力させています。

JUnit形式のxmlを出力させることで、CircleCIがテスト結果の収集や表示を行ってくれます。HTMLのレポート出力は人間用です。

最後に、出力したファイルを保存する処理です。

``` Yaml
      - store_artifacts:
          path: test-reports/
          destination: test-reports

      - store_test_results:
          path: test-results
```

`store_artifacts`を実行することで、コンテナ内に保存した結果をビルド結果のページから見られるようにコピーします。

ここまでで、GitHubへプッシュする毎にテストが実行される状態にできます。追加で、「毎日16時にビルドを実行する」という設定もしてあります。

``` Yaml
workflows:
  version: 2
  normal_workflow:
    jobs:
      - build
  schedule_workflow:
    triggers:
      - schedule:
          cron: "1 7 * * *" # UTCで記述。-9
          filters:
            branches:
              only:
                - master
    jobs:
      - build
```

`cron`の行が、この定期実行の具体的な時間指定です。[ジョブの実行を Workflow で制御する - CircleCI](https://circleci.com/docs/ja/2.0/workflows/#workflow-%E3%82%92%E3%82%B9%E3%82%B1%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B)を参考に、好きな時間に設定してみましょう。

<div style="page-break-before:always"></div>

## 参考

- [Configuring CircleCI - CircleCI](https://circleci.com/docs/2.0/configuration-reference/#store_test_results)
  - テスト結果の格納時、フォルダ名を任意で設定する点の説明など
- [CircleCI を設定する - CircleCI](https://circleci.com/docs/ja/2.0/configuration-reference/)
  - YAMLの書き方