読者です 読者をやめる 読者になる 読者になる

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

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

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

音樂はSoundCloud等バラバラの場所に公開中です。申し訳ないがlinkをたどるなどして探してください。

考察は現在は主に此のblogで公表中です。

programmingは、ひろくみせるものはGitHubで、個人的なものはBitBucketで開発中です。

c4se

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

Programming Python

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

大体わからう。

ハイ。