PyCon APAC 2013参加レポート

第2回 Pythonによる開発運用を助けるツールたち ─パッケージシステム,DataDogでモニタリング,mockを使ったテスト

この記事を読むのに必要な時間:およそ 8 分

about mock

1日目の最後のセッションはBeProudで働く@podhmoさんによるmockライブラリに関するお話です。Pythonで何かを作るより遊ぶことが多いpodhmoさんは,mockライブラリの使い方はもちろんのこと,どのようにこのライブラリが実装されているのか,自分で実装するにはどうすれば良いのかをコードを示しながら発表していました。

mockとは何か

mockはテスト時に都合の良いように,保存先を置き換えるライブラリです。mockではなく正確にはtest doubleというようです。mockは以下の2つの機能を含みます。

Stub
実行時に好きな値を返すようにします。入力値を置き換える機能のことを言います。
Spy
実行時にどのように使われたか確認する。出力値の調査を行う機能のことを言います。

ただ,いろいろと言葉の定義が揺れる場合があるようなので注意が必要です。この発表では上記の定義で話を進めていました。

mockライブラリの使い方

まずはStubの使い方です。ここではtarget.pyのis_holiday()の中でdatetime.now()を呼んでいます。テストが呼ばれたタイミングで,datetime.now()が実行されると,実行結果が都度変わってしまいます。そこでtests.pyでは,is_holiday()自体をmockライブラリを使ってStubに置き換えています。ちなみに,テスト対象を_callFUT()というメソッドに書くのは慣習らしいですね。

target.py::

    from datetime import datetime

    def is_holiday():
        return datetime.now().weekday() == 0

    def visit_shop(name):
        if is_holiday():
            fmt = "{name} is close."
            return fmt.format(name=name)
        else:
            fmt = "{name} is open."
            return fmt.format(name=name)

tests.py::

     import mock
    import unittest

    class Tests(unittest.TestCase):
        def _callFUT(self, *args, **kwargs):
            from target import visit_shop #2.x
            return visit_shop(*args, **kwargs)

        @mock.patch("target.is_holiday")
        def test_it(self, m):
            m.return_value = True
            result = self._callFUT("Shop")

            expected = "Shop is close."
            self.assertEqual(result, expected)

続いてSpyの使い方です。ここでは,after_submit()を呼んだ時に,enqueue_data()とnotify_message()が適切な引数で呼ばれたことを確認することが目的です。そこで,これらの関数をmockライブラリを使って,Stubに置き換え,Spyの機能(assert_called_once_with)を使って引数をチェックしています。

target.py::

    def enqueue_data(data):
        # using external resource! (e.g. MQ)
        message = "Enqueue Messaging Queue!"
        raise Exception(data, message)

    def notify_message(name):
        # using external resource! (e.g. mail)
        fmt = "{name} submitted!"
        raise Exception(fmt.format(name=name))

    def after_submit(data, name):
        enqueue_data(data)
        notify_message(name)

tests.py::

    import mock
    import unittest

    class Tests(unittest.TestCase):
        def _callFUT(self, *args, **kwargs):
            from target import after_submit
            return after_submit(*args, **kwargs)

        @mock.patch("target.notify_message")
        @mock.patch("target.enqueue_data")
        def test_it(self, m0, m1):
            data = mock.sentinel.Data
            self._callFUT(data, "Foo")

            m0.assert_called_once_with(data)
            m1.assert_called_once_with("Foo")

このように,mock.patch()を使っていれば,StubもSpyの機能も簡単に使うことができます。

mockを実装する

ここから,podhmoさんの本領発揮です。mockを自前で実装していきます。mockの実装に必要なのは以下の3つの機能です。

  • 模倣(mimic, fake)
  • 置き換え(patch)
  • 記録(capture)

まずは模倣から実装します。以下のMockクラスを自前のMockクラスに置き換えることを目的とします。

import mock

def complex_query(qs, name):
    qs = qs.where(name=name).where(pemission_id=1)
    return qs.where(deleted_at=None).as_list()


m = mock.Mock()
m.where.return_value.where.return_value.where.return_value.as_list.return_value = ["Foo"]

complex_query(m, "Foo") # => ["Foo"]

これで必要なのは.(ドット)アクセスの模倣と関数呼び出しの模倣です。これらを模倣するために,__dict__と__call__を利用します。Pythonの属性は基本的に__dict__属性にdict型で格納されているだけです。同様に,関数呼び出しは__call__メソッドが実装されているか否かだけで判断されます。これらを利用すれば,以下のようにmockを簡単に実装できます。

class MyMock(object):
    def __init__(self, name='*'):
        self.name = name

    def __getattr__(self, k):
        c = self.__class__(name=k)
        setattr(self, k, c)
        return c

    def __call__(self, *args, **kw):
        if hasattr(self, "return_value"):
            return self.return_value
        raise Exception("not callable")

この他にもisinstance()の模倣や置き換え(patch)⁠記録(capture)に関して実際にコードを示しながら,解説がありました。今回の発表で使用したMyMockはgistに挙がっています。かなりシンプルです。

greeting.py
https://gist.github.com/podhmo/6497807

mockライブラリを使ったテストの欠点

画像

テストに関する簡単なマトリクスがスライドに映され,この左下のTOOOOO-BAD!が最悪なテストだとpodhmoさんは説明していました。

テストが成功しているのに,実際にアプリを動かしたときにエラーで落ちるパターンです。これはmockを使ってテストを書いていると良く起こる現象です。特にリファクタリングの後ですね。以下の例が紹介されていました。

ここではリファクタリングによって,Book.ppメソッドをBook.normalizeメソッドに変更したようですが,テストを変更していなかったようです。

target.py::

    class Book(object):
        def __init__(self, content):
            self.content = content

        def normalize(self): #dont provide pp
            return self.content #do_something()

    class Event(object):
        def __init__(self):
            self.events = []

        def publish(self, v):
            self.events.append(("publish", v))

    def publish_pp(event): #expected pp
        book = Book("user.get_book()")
        event.publish(book.pp())

tests.py::

    import unittest
    import mock

    class Tests(unittest.TestCase):
        def _callFUT(self, *args, **kwargs):
            from target import publish_pp
            return publish_pp(*args, **kwargs)

        @mock.patch("target.Book")
        def test_it(self, m): #expected pp
            m.return_value.pp.return_value = "mock"
            from target import Event
            ev = Event()
            self._callFUT(ev)
            expected = ("publish", "mock")
            self.assertEquals(ev.events[0], expected)
            m.return_value.pp.assert_called_with()

tests.pyではBookクラスをStubで完全に置き換えてしまっています。単にpatch()を使ってStubに置き換えただけでは,このStubは元のクラスがどのような属性やメソッドを持っているのか,知りません。Stubで置き換えた対象がppメソッドを持っている前提でテストが書かれているために,テストが通ってしまいます。

こういう時はspecを使うと属性のチェックも行ってくれます。patchの引数にspec=[対象のクラス]を渡すだけですね。

tests.py::

    import unittest
    from mock import patch

    class Tests(unittest.TestCase):
        def _callFUT(self, *args, **kwargs):
            from target import publish_pp
            return publish_pp(*args, **kwargs)

        def test_it(self): #expected pp
            from target import Book
            with patch("target.Book",spec=Book) as m:
                m.return_value.pp.return_value = "mk"
                from target import Event
                ev = Event()
                self._callFUT(ev)
                expected = ("publish", "mock")
                self.assertEquals(ev.events[0], expected)
                m.return_value.pp.assert_called_with()

この他にautospecの紹介がありましたが,specとautospecを駆使しても,以下の問題は残るようです。

  • プロパティとメソッドの区別が難しい
  • インスタンス変数を関知しない

podhmoさんは,mockライブラリは頑張っているが,これを使ったテストではオブジェクト間のインターフェースの変更に弱い問題はいくつか残ってしまうと話していました。

この発表はmockライブラリについて詳しく説明されており,ユニットテストを書く際の参考になります。技術的なメリットとデメリットも明確に示してあるので,これを参考にテストの書き方の基準を定められるように思いました。また,mockライブラリを自前で実装したりと,Pythonの内部構造に関しても理解できる,一石二鳥なお話だったと思います。

次回は2日目のセッションとパーティやランチを含め,会場の様子をお伝えしようと思います。

著者プロフィール

藤原敬弘(ふじわらたかひろ)

FULLER株式会社

1986年生まれ。北海道苫小牧市出身。苫小牧工業高等専門学校卒業。

Fuller, Inc. CTO

Webプログラマ,よく利用する言語はPython。Pythonコミュニティによく出没する。趣味でArduinoやRaspberry Piなどを使って,便利なものを自作する。

twitter:@wutali
github:https://github.com/wutali

バックナンバー

PyCon APAC 2013参加レポート