unittestのmockでハマった3つのこと


外部ライブラリをpatchするとき

  • nltkなど外部ライブラリのメソッドを呼び出す関数があったとする。
# ./nltkclient.py
from nltk.tag import pos_tag

def normalize():
    morph = [...]
    pos_tag(morph)
    ...
  • この関数をテストするときにnltk.tag.pos_tagメソッドについてはモックをパッチさせてテストを実行したい。
# ./test_nltkclient.py
from unittest import TestCase
from unittest import mock

class NltkClient(unittest.TestCase):
    # ↓パッチした。
    @mock.patch('nltk.tag.pos_tag')             
	def test_normalize(self, pos_tag):
    	pos_tag.return_value = [('word', 'tag')]
  • しかし、この書き方だとpos_tagのモックはただしくパッチされませんでした。
  • 外部ライブラリをモックとして扱うには以下のように書く必要があった。
# ./test_nltkclient.py
class NltkClient(unittest.TestCase
	× @mock.patch('nltk.pos_tag')
    @mock.patch('nltkclient.pos_tag')             
	def test_normalize(self, pos_tag):
    	pos_tag.return_value = [('word', 'tag')]
    	

return_valueの値がおかしい??

  • 以下のように複数のオブジェクトをパッチするとき
@mock.patch('tasks.pos_tag')
@mock.patch('tasks.tokenize')
@mock.patch('tasks.normalize')
def test_normalize(self, pos_tag, tokenize, normalize):
	pos_tag.return_value = [('mocked', 'VERB'), ('text', 'NOUN')]
    tokenize.return_value = ['mocked' ,'text']
    normalize.return_value = None
  • return_valueの値が正しくならない。

  • ここで注意したいのは、デコレータは内部から引数に入っていることです。したがって、引数はデコレータを書いた順と逆にする必要があることです。

@mock.patch('tasks.normalize')
@mock.patch('tasks.tokenize_text')
@mock.patch('tasks.pos_tag')
def test_normalize(self, pos_tag, tokenize_text, normalize):
	pos_tag.return_value = [('mocked', 'VERB'), ('text', 'NOUN')]
    tokenize_text.return_value = ['mocked' ,'text']
    normalize.return_value = None

クラスのインスタンスをモックさせる

  • 例えば、Celeryというタスクキューライブラリを使っていてAsyncResultはタスクの状態を監視できるクラスである。
  • テストでは、実際にタスクを走らせずに実行されているかどうかのみをテストしたいとする。
  • AsyncResultはtask.idをイニシャライザ引数として受け取りインスタンス化させる。属性としてstatusとresultを持ちそれぞれが返す値をモックさせる。
from unittest import mock

from rest_framework import status
from rest_framework.test import APITestCase

from apiv1.views import AsyncResult


class resultTrackViewTest(APITestCase):
    
    # ↓ @mock.patch.object(クラス, メンバ, 返す値(メンバがメソッドならcallableである必要がある)
    @mock.patch.object(AsyncResult, '__init__', lambda self, task_id: None)
    @mock.patch.object(AsyncResult, 'status', 'SUCCESS')
    @mock.patch.object(AsyncResult, 'result', 'write_path')
    def test_get_response(self):
        response = self.client.get(f'/api/v1/tasks/{uuid.uuid4()}/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data, 'write_path')
  • 特に__init__メソッドについては、selfとtask_idを引数に受け取りNoneを返すラムダを指定することによってうまく動作しました。
  • ふたつ前の章同様にAsyncResultはcelery.result.AsyncResultではなく、apiv1.views.AsyncResultなどそのライブラリを呼び出しているパスに対してモックを適用するように記述することに注意する。

参考にした記事