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 ofdatetime.date
with atoday()
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.datetime
はdatetime
モジュールの
datetime
クラスなんだな
でもtest_date
が置き換えるのはdatetime.date
クラス
いいんだろうか
いいことにしよう
次はReplacer
とreplace()
[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()
こっちがきっと本の意図に違いない