c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

Pythonのunittestを書かう。其れを自動実行しやう (rake test + guardを置き換へやう)

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 ドキュメント

定番っぽいのと云へばpytesttoxなのだらうが、附属のものを使ってみる。後で 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 で謂ふ CapistranoMina) にも採用されてゐる。

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()

大体わからう。

ハイ。