tl;dr

追記:後日談を書きました inokara.hateblo.jp tl;dr 日本の祝日や休日を JSON で返してくれる Holidays JP API というサービスがありますよね. holidays-jp.github.io すごく便利で有難いと思います. 今回は, ちょっとした時間が出来たので Holidays JP API と同様...

inokara.hateblo.jp

前回の記事の続きというか, 前回, 突貫で作った python スクリプトを自分なりに作り直してみました.

スクリプトを作り直すにあたって, テストを書いたり, その上で Python 3 系の複数のバージョンでテストを Travis CI で回すようにしてみたり, モックを使ったり, 色々と経験出来たので覚書として残しておきたいと思います.

尚, あくまでも「自分なりに」なので, 誤り等あればご指摘頂けると幸いです.

作ったもの

holidays.py - 内閣府が提供する祝日・休日情報の csv を JSON フォーマットで生成して Amazon S3 のバケットに保存する Python スクリプトです.

github.com

使い方とかは README をご一読下さい.

内閣府が提供している祝日・休日 csv データですが, 以前はそのフォーマットがとても使いづらいと話題に上がっていたようで, すごく苦労するんだろうなあと思っていましたが, 現在では shift-jis 形式で保存されている以外, ネガティブな感情を抱くことはありませんでした.

知見

requests.get() を mock で置き換える

csv データを取得する為に, requests モジュールを利用していますが, テストの度に内閣府のサーバーにアクセスするのはイケていません. そこで, テストを実行する際には, requests.get() のレスポンスをモックオブジェクトに置き換えることにしました.

以下, csv データを取得する関数です.

def getHolidayCsv():
    '''祝日 csv データ内閣府より取得する
    '''
    try:
        res = requests.get('http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu_kyujitsu.csv', timeout=3)
        res.raise_for_status()
    except requests.exceptions.HTTPError as err:
        print(err)
        sys.exit(1)

    return res.content

以下, そのテストです.

  @mock.patch('requests.get')
   def test_get_holiday_csv(self, mock_get):
       res = requests.Response()
       res.status_code = 200
       res._content = ''
       mock_get.return_value = res
       self.assertEqual(holidays.getHolidayCsv(), '')

上記のように, デコレータで requests.get() にパッチを当てることで, モックオブジェクトを検証するようにします.

実際にテストを実行すると, 以下のようにテストは通ります.

$ python -m unittest tests.test_holidays.HolidaysPyTest.test_get_holiday_csv -v
test_get_holiday_csv (tests.test_holidays.HolidaysPyTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

S3 への put_object を moto で置き換える

S3 へのアクセスも requests.get() と同様に, テストの度に S3 にアクセスするのは筋が悪いので, お馴染みの moto に boto3 の put_object 関数を振る舞わせたいと思います.

moto - Moto is a library that allows your python tests to easily mock out the boto library

github.com

実施には, holidays.saveYearsData()holidays.putObject() を呼んでいるので, 少し分かり辛い感じになってしまいましたが, holidays.putObject() の中で呼ばれている s3.put_object() の振舞いを moto が肩代わりするイメージです.

 @mock_s3
   def test_save_years_data(self):
       s3 = boto3.resource('s3', region_name='ap-northeast-1')
       s3.create_bucket(Bucket=os.getenv('BUCKET_NAME'))

       contents = {'2017-01-01': '元日',
                   '2018-12-24': '振替休日',
                   '2019-01-14': '成人の日'}
       holidays.saveYearsData(contents)

       data = '{"2017-01-01": "元日"}'
       body = s3.Object('holiday-py', '2017/data.json').get()['Body'].read().decode("utf-8")
       self.assertEqual(body, data)

       data = '{"2018-12-24": "振替休日"}'
       body = s3.Object('holiday-py', '2018/data.json').get()['Body'].read().decode("utf-8")
       self.assertEqual(body, data)

       data = '{"2019-01-14": "成人の日"}'
       body = s3.Object('holiday-py', '2019/data.json').get()['Body'].read().decode("utf-8")
       self.assertEqual(body, data)

moto については, 以前にも触れたことがありますが, ユニットテストで使う場合に便利だと思います.

invoke コマンド

以前にもちょっと触れたことがあるけど, Ruby の Rake っぽいタスクランナーが欲しくて invoke を試してみました.

invoke - Pythonic task management & command execution.

github.com

task.py は以下のように書いておきます.

import sys
from invoke import run, task

@task
def readme(context):
    try:
        run("gh-md-toc --insert README.md && rm -f README.md.*.*")
    except Exception:
        sys.exit(1)


@task
def test(context):
    try:
        run("python -m unittest tests.test_holidays -v")
    except Exception:
        sys.exit(1)

以下のようにコマンドラインから実行します.

invoke test

以下のように run 関数で指定した unittest が走ります.

$ invoke test
test_convert_dict (tests.test_holidays.HolidaysPyTest) ... ok
test_decode_content (tests.test_holidays.HolidaysPyTest) ... ok
test_get_holiday_csv (tests.test_holidays.HolidaysPyTest) ... ok
test_get_years (tests.test_holidays.HolidaysPyTest) ... ok
test_save_all_data (tests.test_holidays.HolidaysPyTest) ... ok
test_save_years_data (tests.test_holidays.HolidaysPyTest) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.824s

OK

Travis CI を使って, 複数の Python バージョンでテスト出来るようにする

今回の主旨とは離れてしまいますが, ちょっとした思いつきで Travis CI でテストを回してみようと思い, 以下のように .travis.yml を用意しました

sudo: false
language: python
python:
  - 3.4
  - 3.5
  - 3.6
script:
  - invoke test

これだけの設定 (実際には Travis CI の Web コンソールからリポジトリを指定する必要がありますが) で, 簡単に複数の Python バージョンでテストを流すことが出来ました.

実際にテストを流した状態は下図ような感じになります.

イイ感じですね.

以上

メモでした.

元記事はこちら

内閣府が提供する祝日・休日 csv データをよしなに JSON フォーマットに変換して Amazon S3 に保存する Python スクリプトを書いてみたのと, そこで得たイイ感じでテストを書く, テストを回す為の知見を幾つか