Pythonでコンストラクタのキーワード付き引数をモックのアトリビュートにセットする

ORMに[peewee](http://docs.peewee-orm.com/en/latest/)を使うクラスをユニットテストしています．
テストには[mock](https://pypi.python.org/pypi/mock)を使い，モデルのアトリビュートに正しい値をセットしているか確認しようとしましたが，モデルのコンストラクタにアトリビュートにセットするものをキーワード付き引数で渡す場合に，mock.Mockクラスそのままではうまくテストできなかったので，キーワード付き引数をアトリビュートにセットするモックを作成しました．

なお，Python歴がまだ2ヶ月なもので，大きな勘違いをしているかもしれず，もっとスマートなやり方があるのかもしれません．間違いやより良い方法を知っている方がいればコメント頂けると大変助かります．

> 2016/10/15追記
> この記事のコードではモックが1インスタンスだけ作成されるケースにしか対応していません．複数インスタンス化されるケースは，コメントで @podhmo さんが[例示してくれたコード](https://gist.github.com/podhmo/c1f1c562149b1dd9b7bd619c31c48654)のようなやり方が必要です．


## 環境

* Python 2.7.12
* peewee 2.8.5
* mock 2.0.0

## 準備

```bash
$ pip install peewee
$ pip install mock
```

## ディレクトリ構成

```bash
$ 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()
```

```bash
$ 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()
```

```bash
 PYTHONPATH=src python test/test_service.py 
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

これで期待する動作をするモデルのモックが作れました．
mock.Mockにはそもそもnameというアトリビュートがあるようなので少し紛らわしい例でしたが，name以外のアトリビュートで試しても同様でした．

## 参考
* [26.6. unittest.mock — 入門 — Python 3.5.2 ドキュメント](http://docs.python.jp/3/library/unittest.mock-examples.html#partial-mocking)
* [[python] まだmockで消耗してるの？mockを理解するための3つのポイント - くろのて](http://note.crohaco.net/2015/python-mock/)
* [Pythonでオブジェクトに対し、動的にプロパティやメソッドを追加する - Symfoware](http://symfoware.blog68.fc2.com/blog-entry-874.html)