質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.80%

python3unittestで、クラスのインスタンス変数をpatchでmock化したい

受付中

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,700

siruku6

score 996

models/client.py 内のクラスのインスタンス変数hogeをmock化したいです。
tests/test.py にテストコードを書いています。

ディレクトリ構成

app/
├ models/client.py

└ tests/test.py

ソースコード

大体こんな感じなのだろうなと予想をしております。

# client.py
class Client():
    def __init__(self):
        # ...色々省略しています...
        self.hoge = # 外部APIに対するrequestのresponse結果が代入される

    def get_hoge(self):
        result = # self.hoge をここでごちゃごちゃといじる
        return result

    # 以下インスタンスメソッドが続きます


# test.py
import unittest
from unittest.mock import patch
import models.client as client

class TestClient(unittest.TestCase):
    def setUp(self):
        self.client_instance = client.Client()

    def test_get_hoge_method(self):
        with patch('models.client.???', ???=???):
            hoge = self.client_instance.get_hoge()
        self.assertEqual(hoge, 望ましい結果, 'get_hogeがresponseに対して正しく操作を行っていることを確認する')

やりたいこと

テストにおいて実際にAPIに対してrequestしたくないので、self.hogeをmockで仮想responseに置き換える。

試したこと

???と書いた部分を試行錯誤してなんとかうまく動いてはいるのですが、ものすごく遠回りをしていて、実際誰もこんな書き方しないだろうな...というひどいコードになってしまいました。

お願い

ですので、上記の目的を達成できる方法について、ご存知の書き方があれば複数提供いただけると大変嬉しいです。

また、unittestのpatchの書き方を上手く検索することができなかったため、何か良い検索方法をご教示いただけると大変嬉しいです。

ドキュメントの類を見ても、自分の欲しい書き方がどこにあるのか全く分かりませんでした。
たとえばここ
unittest.mock --- 入門¶

追記

インスタンス変数をmockで置き換えることができた例(すごく汚いコード)

(2019/05/02 追記)

# client.py

import abcd.something_api as something_api
class Client():
    def __init__(self):
        # ...色々省略しています...
        something_api.Reqetsing(some_object)
        self.api_response = some_object.response

    def get_hoge(self):
        # self.api_response をここでごちゃごちゃといじるのですが省略します
        result = self.api_response['hoge']
        return result

    # 以下インスタンスメソッドが続きます


# test.py
import unittest
from unittest import mock
from unittest.mock import patch
import models.client as client

class TestClient(unittest.TestCase):
    def setUp(self):
        self.client_instance = client.Client()

    def test_get_hoge_method(self):
        response_msg = 'dummy_responseです'

        _mock = mock.MagicMock(response={ hoge: response_msg })
        with patch('abcd.something_api.Requesting', side_effect=[_mock]):
            # client.py の `something_api.Reqetsing` も mock で置き換えた
            with patch('***.***.***', return_value=None):
            hoge = self.client_instance.get_hoge()

        self.assertEqual(hoge, response_msg, 'get_hogeがresponseに対して正しく操作を行っていることを確認する')


メソッドの細部の書き換えや省略をしているので誤記があるかもしれませんが、ほぼこのような動きです。

インスタンス変数をmock化したい理由

mockとpatchで2回の置き換えを行っているのが不満で、どうにかこれを「1回で済ませたいな」という思いから、インスタンス変数をmock化したいと思っています。

それだけでなくインスタンス変数をmock化する方法自体も純粋に知りたいので、できればメソッドのmock化ではなく、インスタンス変数のmock化の方法を回答いただきたいです。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

+1

ご提示のサンプルがどこまで実際のコードに近いのかわかりませんが、ご提示のコードの場合だと、単純にテスト内で

self.client_instance.hoge = 'hoge'

とすればよいだけではないでしょうか。 unittest.mock を使う必要が無いように見えます。

実際のコードはもっと mock を使いたくなるようなものになっているのでしょうか。もしそうなら、もう少し実際のものに近いコードをご提示いただいた方がより具体的な回答がもらいやすくなると思います。

追記 1

インスタンスアトリビュートではなく、外部 API へのリクエスト処理を mock 化したい、ということですね。

ひとつだけサンプルをお示ししてみます。

次のようなクラス Client がある場合を考えてみます。

client.py:

import requests

class Client:
    URL = 'https://example.com/api/posts/'

    def get_hoge(self):
      response = requests.get(self.URL)
      return response.content

ここで requests.get() は外部 API へのリクエストを行っていると考えてください。

この requests.get() をコンテキストマネージャで mock したい場合は、例えば次のようにします。

test_client.py:

import unittest
from unittest import mock

import client

class TestClient(unittest.TestCase):
    def test_get_hoge(self):
        with mock.patch('client.requests.get') as get:
            get.return_value.content = 'mocked value'
            c1 = client.Client()
            result = c1.get_hoge()
            self.assertEqual(result, 'mocked value')

私の手元ではこのテストは成功しますが、これでご質問への回答になっているでしょうか。あるいはもっと違ったものを求めてらっしゃるでしょうか。

何か良い検索方法をご教示いただけると

私は公式ドキュメントの unittest.mock のページ ↓ を理解できるまで読むのが結局近道だと考えていて他にこれといったものも思いつけないので、他の方からの回答を待ってみてください :D

もしかしたら、 GitHub で from unittest.mock import patch 等で検索するとよいサンプルが見つかるかもしれません:

追記 2

追記・コメントありがとうございます。

できれば、メソッドではなくて変数をmock化する方法がしりたいのですが、そういったことはできないのでしょうか?

インスタンス変数を置き換える方法を探していまして、一応実現できた方法を追記しますので、それよりももう少しマシな方法があればなぁと思っております。

このご発言から siruku6 さんが「自分が追記したコードではインスタンス変数の置き換えがなされていて、 gh640 のコードではされていない」と考えていらっしゃると私は理解したのですが、この理解は合っていますか?

もしそうなら、私は違う認識を持っています。私は siruku6 さんが追記されたコードも私が提示したコードも mock 化という意味ではやっていることは同じで「 callable を差し替えて、その戻り値のインスタンスのアトリビュートをダミー値に変えてチェックしている」と思っています。 siruku6 さんのコードでは callable が abcd.something_api.Requesting でアトリビュートが response 、私が提示したコードでは callable が requests.get() でアトリビュートが content と、それぞれ名前は違いますが構造的には同じだと私は認識しています。

いかがでしょうか。もしこの理解 ↑ が間違っているなら、私は siruku6 さんが繰り返し言われている「インスタンス変数の mock 化」が何を意味するのかがわかっていません。「インスタンス変数の mock 化」が、私のコードではされていなくて siruku6 さんのコードではなされている、と考えていらっしゃるかと思うのですが、そこをもう少し詳しくご説明いただけると、何らか追加の説明がさせていただけるかもしれません。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/29 12:55 編集

    実際のコードを全部乗せるとややこしくなるため、省略しています。
    かといって適度に複雑にするというのもなかなか難しかったため極限まで単純化してしまいました。
    実際 hoge の中には、外部APIに対するrequest結果が代入されていて、その仮想レスポンスをget_hoge()関数でごちゃごちゃといじって返却する想定です。

    説明不足なところがあったかもしれませんが、
    「インスタンス変数をmockによって置き換えるための汎用的な方法が知りたい。」
    という目的で質問しておりますので、他の方法でお願いしたいです。

    上記コードにおいては確かにmockは必要ないかと思いますが、教科書類では、明らかに必要ない場所で無駄に高度な手法を使う説明がされることがあると思います。
    そういう観点で回答いただけると大変ありがたいです。

    ※念のため補足しておきますが、with patch(): という記法で置き換えたいです。
    他の方法は今は求めておりません。

    キャンセル

  • 2019/04/29 20:57 編集

    追記ありがとうございます!
    非常に理想に近い形になってきました。

    できれば、メソッドではなくて変数をmock化する方法がしりたいのですが、そういったことはできないのでしょうか?

    インスタンス変数を置き換える方法を探していまして、一応実現できた方法を追記しますので、それよりももう少しマシな方法があればなぁと思っております。

    ドキュメント類は、、、自分なりに頑張って読んだのですが、追記したコードが現時点の私の限界でした。

    キャンセル

  • 2019/05/06 12:16 編集

    >「インスタンス変数の mock 化」が何を意味するのか
    return_value=変数
    では、メソッドを呼び出したときの返り値を指定することはできます。
    が、そうではなくて、変数を呼び出したときの値を設定したいのです。
    ()がついているときの戻り値ではなく、()がないときの戻り値を設定する方法とも言えます。

    クラス内にインスタンスメソッドとインスタンス変数が宣言されているとして、
    ・self.hoge() に対しては return_value で戻り値をmockで設定
    ができますが、
    ・self.hoge に対して、代入されている値(返される値)をmockで設定
    をする方法が知りたいです。

    return_valueだと、変数に代入されている値を操作することができなかったため、私は side_effect を使って無理やり実現したのですが、side_effect以外の方法があれば知りたいです。

    ただのmockならなんだかできそうなのですが、with patchで実現する方法がなかなか見つかっていません。

    # ???の部分の記述だけでなんとかしたい(side_effect以外)
    with patch('someclass.hoge', ???=代入する値):

    キャンセル

  • 2019/05/07 13:55

    ありがとうございます。おかげさまで「インスタンス変数の mock 化」の意味はわかりました。

    話を続ける前に 2 点確認させてください。

    1.

    > return_valueだと、変数に代入されている値を操作することができなかったため、私は side_effect を使って無理やり実現したのですが、side_effect以外の方法があれば知りたいです。

    `side_effect` についての認識について少し気になったのですが、ご提示のコードだと次の 2 行は

    ```python
    _mock = mock.MagicMock(response={ hoge: response_msg })
    with patch('abcd.something_api.Requesting', side_effect=[_mock]):
    ```

    次の 2 行で置き換えられる(つまり、これら 2 つのコードはこの文脈では等価だ)と私は思っていますが、この認識は siruku6 さんも同じですか?

    ```python
    _mock = mock.MagicMock(response={ hoge: response_msg })
    with patch('abcd.something_api.Requesting', return_value=_mock):
    ```

    2.

    ```python
    def __init__(self):
    # ...色々省略しています...
    something_api.Reqetsing(some_object)
    self.api_response = some_object.response
    ```

    私は、ご提示のテストコードから逆算で考えて、 `something_api.Reqetsing()` の戻り値が `some_object` だ(つまり、 `some_object` が `something_api.Reqetsing()` の引数になっているこの ↑ コードは間違っている)と私は思っていたのですが、実際どちらでしょうか。そして、もし `some_object` が `something_api.Reqetsing()` の戻り値でないのであれば、 `some_object` はどこで生成されているのでしょうか?

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.80%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る