Python アプリケーション毎に環境を分ける (pyenv + virtualenv (or venv) + pip)の續きのやうなもの。
Ruby | Python | |
---|---|---|
Lint | Rubocop | flake8 |
テスト | RSpec | unittest |
モック | RSpec | mock (or unittest.mock) |
時間に依存したテスト | Timecop (or ActiveSupport::Testing::TimeHelpers) | freezegun |
テストランナー | Rspec | unittest |
タスクランナー | Rake | Invoke |
ファイル監視 | Guard | watchdog + Invoke |
Lint
追記 20161107 flake8-pep257 は flake8-docstrings に置き換へられた。
PEP8 と PEP257 と云ふコーディングスタイル標準が在る。flake8 を使へば此れに從ってゐるかテスト出來る。
flake8 3.0.2 : Python Package Index
インストールには此う。
pip install pep8 flake8 flake8-pep257
其の儘実行するには問題が在る。PEP257 の D203 と D211 と云ふ規則は互いに矛盾してゐる。洞ちらかに従へばよいので、洞ちらかを無視させる。又一行の長さが 80 文字はいささか短いので、160 文字にしてやらう。
D203: 1 blank line required before class docstring
D211: No blank lines allowed before class docstring
.flake8 と云ふファイルをプロジェクト root に作ってやる。
[flake8] ignore = D203 max-line-length = 160
flake8 を実行するには、flake8
でよい。
因みに以下のコードは docstring や空行等で PEP8 に従はない。地の文が在るから。
テスト & テストランナー
テスト記述とテストランナーには Python 附属の unittest を使ふ。
2 系 25.3. unittest — ユニットテストフレームワーク — Python 2.7.x ドキュメント
3 系 26.4. unittest — ユニットテストフレームワーク — Python 3.5.1 ドキュメント
定番っぽいのと云へばpytestやtoxなのだらうが、附属のものを使ってみる。後で tox にするかも……。
標準のやり方が見當らなかったので當てずっぽうである。tests ディレクトリを作りテストコードを置く。
app.py lib.py tests ├ test_app.py └ test_lib.py
以下のコードだとしやう。
# app.py def succ(n): return n + 1
テストコードは此う成るだらうか。
# tests/test_app.py from app import succ try: import unittest2 as unittest except ImportError: import unittest class TestSucc(unittest.TestCase): def test_succ(self): self.assertEqual(43, succ(42))
變哲も無い。unittest ライブラリのテストディスカバリを使って実行する。
python -m unittest discover -s tests
此れで tests ディレクトリ下から unittest.TestCase を繼承したテストケースを全て実行出來る。
モック
26.5. unittest.mock — モックオブジェクトライブラリ — Python 3.5.1 ドキュメント
Python3 だと附属の unittest の一部と成ってゐるらしい。Python2 には無いので、pip install mock
する。
patch でモックして、呼び出しを assert してみる。次のコードをテストする。
# app.py import lib class SampleException(Exception): pass class Sample(object): def __init__(base): self.base = base def is_even(n): return n % 2 == 0 def sample(self, n): if self.is_even(n): raise SampleException("%d is even." % n) if lib.is_prime(n): return 1 else: return n - 1
機能紹介を意図とした、無理矢理なテストコードを書かう。
# tests/test_app.py from app import ( Sample, SampleException ) try: import unittest2 as unittest except ImportError: import unittest try: import unittest.mock except ImportError: import mock class TestSample(unittest.TestCase): def setUp(self): self.fourtytwo = Sample(42) def test_init(self): self.assertEqual(Sample(1).base, 1) def test_is_even(self): self.assertTrue(Sample(0).is_even()) self.assertFalse(Sample(1).is_even()) self.assertTrue(Sample(2).is_even()) self.assertFalse(Sample(3).is_even()) def test_sample_is_even(self): with\ mock.patch("lib.is_prime") as is_prime, mock.patch.object(self.fourtytwo, "is_even", return_value=True) as is_even: with self.assertRaises(SampleException): self.fourtytwo.sample(2) is_prime.assert_not_called() is_even.assert_called_once_with(2) def test_sample_is_prime(self): with\ mock.patch("lib.is_prime", return_value=True), mock.patch.object(self.fourtytwo, "is_even", return_value=False): self.assertEqual(self.fourtytwo.sample(2), 1) def test_sample_is_not_prime(self): with\ mock.patch("lib.is_prime", return_value=False), mock.patch.object(self.fourtytwo, "is_even", return_value=False): self.assertEqual(self.fourtytwo.sample(2), 1)
patch, patch.object, return_value, assert_called_once_with, assert_not_called を使ってみた。
時間に依存したテスト
時間を計算する式を書いてはテストとして元も子もない時や、閏日等で現在時刻を偽装したい時が在る。又テスト中に時間が経過しては assertEqual に失敗する場合も在るだらう。現在時刻をモックしたいのだ。此れが freezegun である。
spulec/freezegun: Let your Python tests travel through time
from datetime import datetime from freezegun import freeze_time from pytz import timezone try: import unittest2 as unittest except ImportError: import unittest class TestExample(unittest.TestCase): @freeze_time(datetime.now()) def test_freeze(self): self.assertEqual(datetime.now(), datetime.now()) @freeze_time(datetime(2016, 4, 1, 7, tzinfo=timezone("Asia/Tokyo"))) def test_freeze(self): self.assertEqual( datetime.now(timezone("Asia/Tokyo")), datetime(2016, 4, 1, 7, tzinfo=timezone("Asia/Tokyo")) )
タスクランナー
Invoke と云ふ便利なツールが在る。未だ正式版ではないが、Fabricと云ふデプロイツール (Ruby で謂ふ Capistrano や Mina) にも採用されてゐる。
Welcome to Invoke! — Invoke documentation
pip install invoke
する。tasks.py
と云ふファイルを作ると Invoke が其れをタスク定義ファイルとして扱ふ。
from invoke import run, task @task def test(context): try: run("flake8 *.py tests") run("python -m unittest discover -s tests") except Exception: pass
此れで inv test
或いは invoke test
で flake8 と unittest を実行出來る。
ファイル監視
Node.js 界隈には有りふれてゐる。ファイルを変更したら、其れに應じてテストを実行しビルドする仕組みが欲しい。watchdog と云ふライブラリで簡單に実装出來る。
watchdog 0.8.3 : Python Package Index
pip install watchdog
する。inv watch
で、ファイルを更新する度にテストを実行するやうにしてみる。
from invoke import run, task import time from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer class WatcherHandler(FileSystemEventHandler): def __init__(self, context): super(FileSystemEventHandler, self).__init__() self.context = context def on_modified(self, event): test(self.context) @task def test(context): try: run("flake8 *.py tests") run("python -m unittest discover -s tests") except Exception: pass @task def watch(context): observer = Observer() observer.schedule(WatcherHandler(context), ".", recursive=True) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join()
大体わからう。
ハイ。