CirclrCI を使って Python の最低限の単体テストを行うための設定

はじめて CircleCI を触ったので、将来の自分用として作業メモ。
ずっと Jenkins で Docker コマンドを叩いてテスとしていた馬鹿らしくなる位、簡単にテストの設定や GitHub との連携が出来るので驚きました。

## 前提とする知識

本記事では

- Python における Unittest を用いたテストについて
- カバレッジなどテストにおける基本的な概念

の説明は割愛させていただきます。
テストトハナンゾヤ？ドウヤッテカクノダ？と言う方はまずそちらについての記事を読むことをお薦めします。

## テストを行うリポジトリの準備

### テストコードを準備

テストしかない、非常にシンプルなリポジトリを用意しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/0f0de0ec-d939-8faa-55ae-4036e873c89e.png)

行うテストの内容も a と b を比較するだけの非常にシンプルなものです。

```pyhon:test_sample.py
import unittest


class TestSample(unittest.TestCase):
    def test_success_sample(self):
        a = 1
        b = 1
        self.assertEqual(a, b, msg='a is not equals b')


if __name__ == '__main__':
    unittest.main()
```

念のためローカルでテストを実行して問題ないことを確認しておきましょう

```bash:テスト実行結果
$ python -m unittest discover -s tests/
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
```

### CircleCI の設定ファイルを記述

全ての設定項目について知りたい方は[公式ドキュメント](https://circleci.com/docs/2.0/)を参照してください。
今回はテストの実行しか行わないため、ほとんどの機能を使いません(笑)

CirclrCI の設定ファイルは `.circlrci/config.yml` に設置されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/3679fe55-d3bb-0eb5-e56b-0514a7688bf3.png)

ここに色々と設定を書けるのですが、今回使うのは

- version
- jobs

だけです。詳しくは[公式ドキュメントのPythonでのサンプル](https://circleci.com/docs/ja/2.0/language-python/#%E3%83%93%E3%83%AB%E3%83%89%E3%82%B8%E3%83%A7%E3%83%96%E3%81%AE%E4%BD%9C%E6%88%90)を参照して下さい。

#### version の設定
`version` はその名の通りバージョンを指定します。とりあえず `2` もしくは `2.1` を指定しておけば大丈夫かと思います。

#### jobs の設定

ここで、具体的にビルド、テスト、デプロイの手順を記述します。
最低限必要な物は以下の要素になります。

```yaml:config.yml
jobs:
  build:
    working_directory: [コンテナ内での作業ディレクトリ]
    docker:
      - image: [Dockerコンテナ名]
    steps:
      - checkout
      - run:
          command: |
              [実行コマンド]
```

- `jobs` の後は、ジョブ名を指定します。今回、やっている事的には `test` となるべきなのにジョブ名が `build` になっていますが、これは __build というジョブがないとエラーになるため__です。 Python のようなビルドが不要な言語からすると、少し煩わしい仕様になっています。

- `working_directory` では作業ディレクトリ名を指定します。分かれば何でも大丈夫です。今回の例では `~/circleci-test` としています。

- `docker -> image` では実際にビルドやテストが走るイメージを指定します。[CircleCI 自体が CircleCI で使いやすいようにカスタマイズしたイメージを配布している](https://circleci.com/docs/ja/2.0/circleci-images/)ので、そちらを使うことをお薦めします。

- `steps -> checkout` では `working_directory` で指定した場所にリポジトリを checkout してきます。
- `steps -> run` で指定したコマンドを実行します。今回はコマンドを1つしか実行しませんが、今後のことを考えて複数行受け取れるようにしています。(詳しくはYamlの仕様を参照して下さい。)

#### できあがり

最終的に次のような設定ファイルが出来上がります
気をつけて欲しい点として、 デフォルトだと `/usr/local/bin` に PATH が通っていないので、python はフルパスで指定する必要があります。

```yaml:config.yml
version: 2.1
jobs:
  build:
    working_directory: ~/circleci-test
    docker:
      - image: circleci/python:3.7.3
    steps:
      - checkout
      - run:
          command: |
            /usr/local/bin/python -m unittest discover -s tests/

```

これでリポジトリの準備は完了しました。__が、まだ CirclrCI 側の設定が完了していないため GitHub への PUSH はしないでください。__

## CircleCI の Web での設定

一通り前準備が終わったので、今度は CircleCI の GUI を使って設定をしていきます。
Jenkins と違ってほとんどの設定をファイルに書くので、ここでの作業は本当に最低限のものになります。

### アカウント作成

特に難しい所はないので説明は割愛します。
強いて言うと、 自分のリポジトリのあるサービスと連携してアカウント作っておくと、その後が楽になると思います。

### 監視するリポジトリを設定

まず、右側のメニューから「Setting」を選び、サブメニューから「Projects」を選びます。
すると、こんな感じの画面になるはずです。(circleci-test 以外は私が実際に使っているリポジトリなので無視してください)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/673f51cf-6d78-b7d4-2b66-77c4ecfe03bd.png)

今回は circleci-test というリポジトリをテストで作っているので、その設定を行うため、歯車マークをクリックします。
色々複雑な設定も出来ますが、今は未だ早いです。とりあえず、水色の「Follow Prject」を押しましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/97e54aa6-a36a-4ec3-8050-5926a1696d96.png)

これで、プロジェクトが push された際、自動でビルド(今回の場合はテスト)が走るようになります。
Jenkins と違ってとても楽ですね…！

### 試しに push してみる

先ほど作ったリポジトリを GitHub に Push してみましょう。
少し待つとテストが始まり…


無事テストが通りました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/a905f804-3147-6779-e0d7-f406c88efe97.png)

## おまけ: Coverage を取得する

ここまでで、基本的にテストを行う設定は完了です。が、カバレッジは取得したいですよね？ですよね？
ついでにその設定を行ってみましょう。

とりあえず、カバレッジを取得するため、以下のようなファイルを作成します。

```python:sample.py
def add(a: int, b: int) -> int:
    return a + b


def mul(a: int, b:int) -> int:
    return a * b

```

そして、テストも次のように書き換えます

```python:test_sample.py

import unittest

from sample import add


class TestSample(unittest.TestCase):
    def test_success_sample(self):
        self.assertEqual(add(1, 2), 3, msg='function add is invalid')


if __name__ == '__main__':
    unittest.main()

```

`config.yml` を設定するのですが、ほぼ[公式ドキュメントのPythonでのサンプル](https://circleci.com/docs/ja/2.0/language-python/#%E3%83%93%E3%83%AB%E3%83%89%E3%82%B8%E3%83%A7%E3%83%96%E3%81%AE%E4%BD%9C%E6%88%90) に書かれているので、結果だけ書きます。
以下の設定ファイルで Coverage が取得できるようになります。
Coverage のオプションについては、本筋からそれるのでここでは解説しません。[Coverage.py の公式ドキュメント](https://coverage.readthedocs.io/en/latest/)を参照して下さい。

本当は pipenvなどを使った方が今風だと思うのですが、そこまでやる必要もないテストなので、直接 /usr/local/bin 以下にパッケージをインストールします。

```yaml:config.yml
version: 2.1
jobs:
  build:
    working_directory: ~/circleci-test
    docker:
      - image: circleci/python:3.7.3
    steps:
      - checkout
      - run:
          command: |
            sudo chown -R circleci:circleci /usr/local/bin
            sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
            pip install coverage==5.0a5
      - run:
          command: |
            coverage run -m unittest discover -s ./tests
      - store_test_results:
          path: result.xml

```

ついでに、ローカルでデバッグしていると色々ファイルが生成されるので、 `.gitignore` ファイルに入れておきましょう。

```text:.gitignore
.coverage
htmlcov/
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/5bb2ec32-4bfa-d85c-857f-834cc6fc5bdc.png)

これで準備は整いました、PUSHしてみましょう。

すると、テストの終盤、色々アップロードがはじまって…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/33a6f944-e496-c47e-a9ac-6f694f51e7ce.png)

Artifacts のタブから HTML でのカバレッジが確認出来るようになります！(沢山ありますが、`index.html`が本体です)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/fe7b977a-0a26-b3b9-5555-5fe04d044af6.png)

これで、どこのテストを書いていないかが一目で分かるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/a3b9d386-4993-8418-93ed-1fcb872ddb26.png)

#### ホントのおまけ: .coveragerc に設定を分離

最後に、coverage のオプションは `.coveragerc` に分離した方が、オプションが長くなった際に管理が楽なので、そちらに移動させましょう。

次のようなファイルを作成します。

```ini:.coveragerc
[run]
command_line = -m unittest discover -s ./tests
[report]
omit = */tests/*,*/venv/*
```

これで、run, report 時のオプションを省略できるようになります。

```yaml:config.yml
version: 2.1
jobs:
  build:
    working_directory: ~/circleci-test
    docker:
      - image: circleci/python:3.7.3
    steps:
      - checkout
      - run:
          command: |
            sudo chown -R circleci:circleci /usr/local/bin
            sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
            pip install coverage==5.0a5
      - run:
          command: |
            coverage run
            coverage report
            coverage html
      - store_artifacts:
          path: htmlcov

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/48151ba7-b334-be43-9a5d-c903f223f549.png)

これで PUSH してみましょう。先ほどと同じ結果が得られるはずです。

## 参考

- [CircleCI 公式ドキュメント](https://circleci.com/docs/2.0/)
- [GitHub のリポジトリ: kitadakyou/circleci-test](https://github.com/kitadakyou/circleci-test)
- [CirclrCI上でのジョブ履歴: kitadakyou/circleci-test](https://circleci.com/gh/kitadakyou/circleci-test/tree/master)
- [CirclrCI を使って Python の最低限の単体テストを行うための設定 (本記事の元となったブログの記事です)](https://at-428.blogspot.com/2019/07/circlrci-python_14.html)