ORMにpeeweeを使うクラスをユニットテストしています.
テストにはmockを使い,モデルのアトリビュートに正しい値をセットしているか確認しようとしましたが,モデルのコンストラクタにアトリビュートにセットするものをキーワード付き引数で渡す場合に,mock.Mockクラスそのままではうまくテストできなかったので,キーワード付き引数をアトリビュートにセットするモックを作成しました.
なお,Python歴がまだ2ヶ月なもので,大きな勘違いをしているかもしれず,もっとスマートなやり方があるのかもしれません.間違いやより良い方法を知っている方がいればコメント頂けると大変助かります.
2016/10/15追記
この記事のコードではモックが1インスタンスだけ作成されるケースにしか対応していません.複数インスタンス化されるケースは,コメントで @podhmo さんが例示してくれたコードのようなやり方が必要です.
環境
- Python 2.7.12
- peewee 2.8.5
- mock 2.0.0
準備
$ pip install peewee
$ pip install mock
ディレクトリ構成
$ tree
.
├── src
│ ├── __init__.py
│ ├── model.py
│ └── service.py
└── test
├── __init__.py
└── test_service.py
テスト対象コード
- peeweeのモデルクラスを継承したmodel.HogeModelをservice.HogeServiceが利用してデータストアするサンプルです
src/service.py
# -*- coding: utf-8 -*-
from model import HogeModel
class HogeService(object):
def run(self):
model = HogeModel(name='hoge name')
model.desc = 'hoge desc'
model.save()
src/model.py
# -*- coding: utf-8 -*-
from peewee import SqliteDatabase
from peewee import Model
from peewee import PrimaryKeyField
from peewee import CharField
db = SqliteDatabase('hoge.db')
class HogeModel(Model):
id = PrimaryKeyField
name = CharField()
desc = CharField()
class Meta:
database = db
テストコード
- service.HogeServiceをテストするコードです
- まずは単純にmock.patchのデコレータを使ってmodel.HogeModelをモック化してみました
- これをテスト実行すると3つ目のテストだけNGになってしまいます
test/test_service.py
# -*- coding: utf-8 -*-
import unittest
from mock import patch
from mock import Mock
from service import HogeService
class TestHogeService(unittest.TestCase):
@patch('service.HogeModel')
def test_run(self, mock_hoge_model):
mock = Mock()
mock_hoge_model.return_value = mock
target = HogeService()
target.run()
self.assertEquals(1, mock.save.call_count) # OK
self.assertEquals('hoge desc', mock.desc) # OK
self.assertEquals('hoge name', mock.name) # NG
if __name__ == '__main__':
unittest.main()
$ PYTHONPATH=src python test/test_service.py
F
======================================================================
FAIL: test_run (__main__.TestHogeService)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/otti/.pyenv/versions/2.7.12/lib/python2.7/site-packages/mock/mock.py", line 1305, in patched
return func(*args, **keywargs)
File "test/test_service.py", line 18, in test_run
self.assertEquals('hoge name', mock.name) # NG
AssertionError: 'hoge name' != <Mock name='HogeModel().name' id='4454023888'>
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
コンストラクタのキーワード付き引数をモックのアトリビュートにセットする
- テストが通らないのは,service.HogeServiceで
model = HogeModel(name='hoge name')
と書いてコンストラクタで渡されたものがモックのアトリビュートにはセットされないからでした - そこで,コンストラクタで渡されたキーワード付き引数をモックのアトリビュートにセットし,patchしたクラスのインスタンス化時にそのモックを返すようにしました
- また,モデルにセットされたアトリビュートをテストコードでassertしたいので,生成したモックはテストクラスのインスタンス変数に持つようにしました
test/test_service.py
# -*- coding: utf-8 -*-
import unittest
from mock import Mock
from mock import patch
from service import HogeService
class TestHogeService(unittest.TestCase):
def create_attr_set_mock(self, **kwargs):
self.mock = Mock()
# コンストラクタで渡されたキーワード付き引数をアトリビュートにセットする
for k, v in kwargs.items():
self.mock.__dict__[k] = v
return self.mock
@patch('service.HogeModel')
def test_run(self, mock_hoge_model):
# 対象クラスのインスタンス化時にコンストラクタのキーワード付き引数をアトリビュートにセットするモックを返す
mock_hoge_model.side_effect = lambda **kwargs: self.create_attr_set_mock(**kwargs)
target = HogeService()
target.run()
self.assertEquals(1, self.mock.save.call_count) # OK
self.assertEquals('hoge desc', self.mock.desc) # OK
self.assertEquals('hoge name', self.mock.name) # OK
if __name__ == '__main__':
unittest.main()
PYTHONPATH=src python test/test_service.py
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
これで期待する動作をするモデルのモックが作れました.
mock.Mockにはそもそもnameというアトリビュートがあるようなので少し紛らわしい例でしたが,name以外のアトリビュートで試しても同様でした.