kb84tkhrのブログ

何を書こうか考え中です あ、あと組織とは関係ないってやつです 個人的なやつ

PPP3: 日付関数のテスト

  • 現在日付を扱う関数は、日付を引数に取る関数と、現在時刻でその関数を呼ぶ関数に分けてテストする

関数を分けるとこまでは思いつくけど
現在時刻でテストするのはあきらめかなあと思ってたら
testfixturesのReplacerってやつを使えば可能らしい
力技?
っていうかこれもモックだよね?何か違う?

サンプルコードを入力してみる
そのままではutil.datetimeが見つからないと言われる

ほぼそのまま入力しただけのソース

from datetime import date, timedelta
from testfixtures import Replacer, test_date

def is_last_of_month(d):
    return (d + timedelta(1)).day == 1

def test_is_last_of_month():
    d = date(2011, 11, 30)
    assert is_last_of_month(d), "%s" % d

def test_is_last_of_month_not():
    d = date(2011, 11, 29)
    assert not is_last_of_month(d), "%s" % d

def is_last_of_month_now():
    return is_last_of_month(datetime.now())

def test_is_last_of_month_now():
    with Replacer() as r:
        r.replace("util.datetime", test_date(2011, 11, 30))
        assert is_last_of_month_now()

r.replace("util.datetime", test_date(2011, 11, 30))のところ

datetime.datetimeを置き換えるのではなく、テスト対象がimportしているもの(この例ではテスト対象はutil.datetimeを利用している)を置き換えます。

ってところがミソなんだろうけどこれはどういうことなんだ
テスト対象はis_last_of_month_now()のことなんだろう

return is_last_of_month(datetime.now())datetime
datetime.datetimeではなくutil.datetimeです、ってことなんだろうな
そもそもdatetime.datetimeってなんだ?
うーん
datetime.nowではなく、(自作の)util.datetime.nowを置き換えます」って
ことかなあ
でもr.replaceではutil.datetimeを置き換えているようにも見える
util.datetime.nowを置き換えるんじゃないのかな
どういうことなんだ
ちょっと調べたほうがよさそうだ

[Mocking dates and times — testfixtures 6.9.0 documentation https://testfixtures.readthedocs.io/en/latest/datetime.html]

TestFixtures provides the test_date() function that returns a subclass of datetime.date with a today() method that will return a consistent sequence of dates each time it is called.

test_date()ってやつはtodayを置き換えたdatetime.dateのサブクラスを返すのか
なるほど
どれがパッケージでどれがモジュールでどれがクラスなのか意識してないと
混乱するんだな

datetime.datetimeってのは何だろう

[datetime — Basic date and time types — Python 3.7.3 documentation https://docs.python.org/3/library/datetime.html]

The datetime module supplies classes for manipulating dates and times in both simple and complex ways.
...
class datetime.datetime
A combination of a date and a time. Attributes: year, month, day, hour, minute, second, microsecond, and tzinfo.

datetimeがモジュールで、datetime.datetimedatetimeモジュールの
datetimeクラスなんだな
でもtest_dateが置き換えるのはdatetime.dateクラス
いいんだろうか
いいことにしよう

次はReplacerreplace()

[API Reference — testfixtures 6.9.0 documentation https://testfixtures.readthedocs.io/en/latest/api.html#testfixtures.Replacer]

class testfixtures.Replacer

These are used to manage the mocking out of objects so that units of code can be tested without having to rely on their normal dependencies.

replace(target, replacement, strict=True)
Replace the specified target with the supplied replacement.
Parameters:
target – A string containing the dotted-path to the object to be replaced. This path may specify a module in a package, an attribute of a module, or any attribute of something contained within a module.
replacement – The object to use as a replacement.
strict – When True, an exception will be raised if an attempt is made to replace an object that does not exist.

"util.datetime"が置き換えるべきオブジェクトで、それをtest_date()で置き換えるってわけだ

def is_last_of_month_now():
    return is_last_of_month(datetime.now())

はutil.pyというファイルで定義されているってことになれば辻褄が合うんだな
util.pyとtest_util.pyに分けるてみる

こんなことを言われたり

    def is_last_of_month_now():
>       return is_last_of_month(datetime.now())
E       AttributeError: type object 'tdate' has no attribute 'now'

やっぱりdatetime.datetimeじゃなくてdatetime.dateを置き換えるんじゃ
なかろうか
そうするとis_last_of_month_nowも書き換えなきゃいけないけど・・・

def is_last_of_month_now():
    return is_last_of_month(date.today())

is_last_of_month_nowっていうよりis_last_of_month_todayかなって
気もするけどそこはスルー

$ pytest test_util.py 
================================================================================== test session starts ===================================================================================
platform linux -- Python 3.6.7, pytest-4.6.2, py-1.8.0, pluggy-0.12.0
rootdir: /home/takahiro/study/PPP3-mac/test
plugins: cov-2.7.1
collected 3 items

test_util.py ...                                                                                                                                                                   [100%]

================================================================================ 3 passed in 0.04 seconds ================================================================================

通った
ところでpytestはunittestモジュールを使ってなくてもテストを実行してくれるんだな
関数名がtestで始まってればいい、とかそういうこと?

結局こんなコードになった
これはこれでいいとは思うんだけど
本の意図に沿ってのかどうかはよくわからない

util.py

from datetime import date, timedelta

def is_last_of_month(d):
    return (d + timedelta(1)).day == 1

def is_last_of_month_now():
    return is_last_of_month(date.today())

test_util.py

from datetime import date
from util import is_last_of_month, is_last_of_month_now
from testfixtures import Replacer, test_date

def test_is_last_of_month():
    d = date(2011, 11, 30)
    assert is_last_of_month(d), "%s" % d

def test_is_last_of_month_not():
    d = date(2011, 11, 29)
    assert not is_last_of_month(d), "%s" % d

def test_is_last_of_month_now():
    with Replacer() as r:
        r.replace("util.date", test_date(2011, 11, 30))
        assert is_last_of_month_now()

なおこれでもいける模様

def test_is_last_of_month_now():
    with Replace("util.date", test_date(2011, 11, 30)):
        assert is_last_of_month_now()

これも意図的に避けているのかどうかはよくわからない
執筆時点ではこういう書き方ができなかったという可能性もあるか

でもさあ
dateじゃなくてdatetimeをテストしたいってこともあるよな
dateしかできないとしたらおかしいよね

あっ
test_datetime()っていう関数がある
これか

こうだきっと
ここを直せばあとはうまくいく

from testfixtures import Replace, test_datetime

def test_is_last_of_month_now():
    with Replace("util.datetime", test_datetime(2011, 11, 30)):
        assert is_last_of_month_now()

こっちがきっと本の意図に違いない