ラベル Tests の投稿を表示しています。 すべての投稿を表示
ラベル Tests の投稿を表示しています。 すべての投稿を表示

2013-04-07

mock.patch() が置き換えする対象

mock.patch() の挙動について質問されて、即答できなかったので、簡単に調べ直してみました。

簡単なサンプル

まず以下のようなコードを想定します。

# foo.py その1
import random

def pickup(seq):
    return random.choice(seq)

random.choice() 関数は呼び出すごとに挙動が変わり、テストしにくいので、mock の出番です。

# test.py
from unittest.mock import patch
import foo

with patch('random.choice') as m:
    m.return_value = 0
    assert foo.pickup([1, 2, 3]) == 0

import の仕方を変えるとモックに失敗する

ここで foo.py の書き方を以下のように変えます。

# foo.py その2
from random import choice

def pickup(seq):
    return choice(seq)

pickup() 関数を外側から見た挙動は同じです。しかし、なんということでしょう、test.py を実行すると assert が失敗します。pdb や print を使って調べるとわかりますが、choice() 関数が Mock オブジェクトになっていません。

名前に対してパッチしている

with patch('random.choice') のコンテクストに入るとき、大雑把に以下のようなことが起こります。

import random
random.choice = Mock()

※ 実際には Mock ではなく、そのサブクラス MagicMock ですが、この議論の本質ではないので、Mock で話を進めます。

Python のプロセスでは、モジュールはシングルトンとしてふるまうため、プロセス内で random.choice という名前は、Mock オブジェクトを参照することになります。foo.py その1の中では random.choice という名前を使っているため、Mock オブジェクトを参照します。

ところが foo.py その2 は random.choice という名前を参照しているわけではありません。from random import choice すると、大雑把に以下のようなことが起こります。

import random
choice = random.choice
del random

patch() 適用後、名前の参照先は

  • グローバルな random.choice => Mock オブジェクト
  • foo.py の choice => 元の choice 関数

のようになっています。このため、foo.py その2では choice がモックにならないのです。

名前空間 foo 以下の名前に対して patch を適用する

じゃあ、どうするのかというと、普段は patch('foo.patch') としています。上記の問題が発生するような状況だというのが分かっていれば、これで解決です。しかしながら、foo.py を、その1からその2に実装を変えたとき、テストが fail するけれど、どこがどうなっているのか見つけにくいな、と思いました。

その1のときに、test.py に patch('foo.random.choice') と書くとよいのでしょうか。こうすると foo.py その2に書き換えてテストすると、fail ではなくて patch() 実行時にエラーが出るので、名前がおかしいことに気付くような気がします。

2012-01-15

mock で外部依存なくしてユニットテストする例

mock を使ったニットテストの、シンプルなシーンをまとめました。例として「Twitter アカウントのスクリーンネームを指定し、そのアカウントの最新のツイートの内容を取得する」という機能を実装する場合を考えます。

外部に依存しない機能のテスト

まず、簡単にユニットテストを書けるような場合から考えます。create_user_timeline_url() 関数を定義し、Twitter アカウントの screen_name を渡すと、そのアカウントのつぶやきを取得するために URL が返される、としましょう。たとえば Twitter アカウントのスクリーンネーム wozozo を渡すと、 http://twitter.com/statuses/user_timeline/wozozo.json が返ってくることを確認します。

実装とテストは以下のようになります。URL のテンプレートを定数にしたほうがいいとかありますが、そこは、やっつけです。

# tw.py
def create_user_timeline_url(screen_name):
    """return URL to get screen_name user's timeline"""
    base = "http://twitter.com/statuses/user_timeline/%s.json"
    return base % screen_name
# tests.py
import unittest
import tw

class CreateUserTimelineUrlTest(unittest.TestCase):
    def test(self):
        result = tw.create_user_timeline_url('wozozo')
        expected = 'http://twitter.com/statuses/user_timeline/wozozo.json'
        self.assertEqual(result, expected)

ユニットテストは「外部に依存させずに、既知の入力に対して、期待する出力が得られることを確認する」ことを確認する、ということなので、上記でユニットテストができています。

外部に依存する機能のテスト

続いて get_latest_tweet_text(screen_name) という関数を作ります。これは screen_name を与えると、そのアカウントの最新のツイートのテキストを返してくれる関数です。たとえばユーザ wozozo が、最後に「show!」とツイッターで発言していれば、この関数は文字列「show!」を返します。機能の実装と、テストはこんな感じでしょうか。

# tw.py
import urllib
import json

def get_latest_tweet_text(screen_name):
    """return latest tweet message by screen_name user"""
    url = create_user_timeline_url(screen_name)
    fin = urllib.urlopen(url)
    content = fin.read()
    content_obj= json.loads(content)
    return content_obj[0]['text']

...
# tests.py
import unittests

class GetLatestTweetTextTest(unittest.TestCase):
    def test(self):
        text = tw.get_latest_tweet_text('wozozo')
        self.assertEqual(text, 'show!')

いろいろと問題があります。そもそも wozozo ユーザが最後に show! とつぶやいていないと、確実に破綻します。なので、テストが想定している状況を作り出す必要があります。

だからと言って setUp でいちいち「show!」とつぶやいてからテストするのは、あまりに外部に依存しすぎです。Twitter が落ちていたらテストできないからです。また、テスト項目が大量にあったら、ものすごい勢いで API をコールするわけで、IP アドレスからのアクセスの制限に引っかかります。OAuth が必要な機能を使っていたらアプリやアカウント停止されられるかもです。ですので外部に依存しないようにしたい。

(1件もつぶやいていない場合はエラーがでますが、別の問題なので、今は依存にだけ集中します)

get_latest_tweet_text の依存先は、

  • tw.create_user_timeline_url() 関数
  • urllib.urlopen() および、そこで発生する通信
  • json.loads() 関数

の、3つです。

2つ目の urlopen() は、すでに述べた理由から、ユニットテストでの依存を断ち切りたい箇所です。

3つ目の json.loads() は標準ライブラリの関数です。バグがないという保証ありませんが、Python のパッケージのテストを通っていることを考えると確率としては非常に低いでしょう。この部分は、正常に動作する Python の機能だと考えます。

問題は1 つめです。正直なところ、こういう関数への依存を切り離すかどうか迷っています。比較的シンプルな関数なので、いちいちモックとか作るのかよ、という気分になります。なりますが、ここはユニットテストでは依存させない箇所として扱います。よりどころにしているのは、 Daniel Arbuckle の「Python Testing Beginner's Guide」です。

class testable:
    […]
    def method3(self, number):
        return self.method1(number) * self.method2(number)
    def method4(self):
        return 1.713 * self.method3(id(self))

[…]
Consider method4. Its result depends on all of the other methods working correctly. On top of that, it depends on something that changes from one test run to another, the unique ID of the self object. Is it even possible to treat method4 as a unit in a self-contained test? If we could change anything except method4, what would we have to change to enable method4 to run in a self-contained test and produce a predictable result?
(超訳: method4 の結果の正しさは、他のメソッドがすべて正しく動作することに依存している。それに加えて、self のユニークな ID を取得するなどという、テストごとに値が変わるようなものにも依存している。そんな状況で method4 を自己完結しているものとしてテストできるだろうか? method4 以外に変更を加えたときに、method4 のテストがとおり、かつ、予想できる結果を得られるようにするには、どうしたらいい?)

create_user_timeline_url() の処理はシンプルですが、標準ライブラリよりはバグ率が高いでしょう。また Twitter の都合で URL が変わるかも知れません。そう考えると、create_user_timeline_url を本当に呼び出さずに、この関数の戻り値が既知の入力であるかのようにテストをしたくなってきます。

というわけで create_user_timeline_url と urllib.urlopen をモックにします。

# tests.py
import unittest
import mock
import tw

class GetLatestTweetTextTest(unittest.TestCase):
    @mock.patch('urllib.urlopen')
    @mock.patch('tw.create_user_timeline_url')
    def test(self, create, urlopen):
        # mock create_user_timeline_url
        url = 'http://example.com/wozozo'
        create.return_value = url
        # mock URL reader
        fin = mock.Mock()
        fin.read.return_value = '[{"text":"show!"}, {"text":"tenga"}]'
        # and set it to urlopen's return value
        urlopen.return_value = fin
        # call UUT function
        text = tw.get_latest_tweet_text('wozozo')
        # assert returned text
        self.assertEqual(text, 'show!')
        # assert 'bucho' is passed to create func once
        self.assertEqual(create.call_count, 1)
        self.assertEqual(create.call_args, (('wozozo',), {}))
        # assert URL from create func is passed to urlopen once
        self.assertEqual(urlopen.call_count, 1)
        self.assertEqual(urlopen.call_args, ((url,), {}))
        # assert urlopen().read() is called once w/o args
        self.assertEqual(fin.read.call_count, 1)
        self.assertEqual(fin.read.call_args, ((), {}))

...

順番に見ていきま show。

class GetLatestTweetTextTest(unittest.TestCase):
    @mock.patch('urllib.urlopen')
    @mock.patch('tw.create_user_timeline_url')
    def test(self, create, urlopen):

tw.create_user_timeline_url, urllib.urlopen を mock.Mock オブジェクトと入れ替えます。そして、このtest メソッドの中では、それぞれ create、 url_open という名前でモックにアクセスできます。

        # mock create_user_timeline_url
        url = 'http://example.com/wozozo'
        create.return_value = url

create_user_timeline_url 関数の戻り値を定数で指定しておきます。

        # mock URL reader
        fin = mock.Mock()
        fin.read.return_value = '[{"text":"show!"}, {"text":"tenga"}]'

urlopen() は本来 file-like オブジェクトを返し、これが裏側で通信します。通信させたくないので、Mock を作ります。そして、read() メソッドの戻り値を指定しておきます。本当は、もうちょっと本物のレスポンスに似せたほうがいいかも知れません。

        # and set it to urlopen's return value
        urlopen.return_value = fin

それを urlopen() の戻り値で返されるようにします。これで、urllib.urlopen() は、上で定義した fin が返るようになります。

        # call UUT function
        text = tw.get_latest_tweet_text('wozozo')

で、テスト対象の関数を呼び出します。以下、入出力の確認です。

        # assert returned text
        self.assertEqual(text, 'show!')

まず、戻り値の確認。

        # assert 'bucho' is passed to create func once
        self.assertEqual(create.call_count, 1)
        self.assertEqual(create.call_args, (('wozozo',), {}))

create_user_timeline_url('wozozo') が1度だけ実行されていることを確認します。そして、このときの戻り値が...

        # assert URL from create func is passed to urlopen once
        self.assertEqual(urlopen.call_count, 1)
        self.assertEqual(urlopen.call_args, ((url,), {}))

urlopen() の引数に使われていることを確認。

        # assert urlopen().read() is called once w/o args
        self.assertEqual(fin.read.call_count, 1)
        self.assertEqual(fin.read.call_args, ((), {}))

最後に read() メソッドが引数なしで呼ばれていることを確認して、おわり。

urlopen() など依存先の関数/メソッド呼び出しというのはテスト対象関数からの「出力」なので assert でテストしています。そして、それらの戻り値というのは、テスト対象関数への「入力」なのでモックで既知の値を渡しています。こうすれば「外部に依存させずに、既知の入力に対して、期待する出力が得られることを確認する」というユニットテストができることになります。

まとめ

以上、外部依存する部分を mock で入れ替えるシーンを、書いてみました。シーンとしてはシンプルなんですが、煩雑に思えるかも知れません。どこまでを外部とするのか、というのは未だに課題です。通信するとか、開発中の比較的大きな別モジュールとかは、外部扱いにしています。