Pytnonで、unittest.mock.patch.objectのautospecとside_effectを使って、テスト対象の属性(self.attr)を更新する

Pythonにて、「メソッドを差し替え、テスト対象オブジェクトの属性を更新する」テストコードを作成する機会があったため、メモを残します。

なお、良いタイトルが思い浮かびませんでしたので、mock.object(autospect=True)のサンプルとして考えてください…

 
目次

 

環境

  • Python 3.6.0
    • unittest.mock.patch.objectを使用
  • pytest 3.0.6
    • テストランナーとして使用

 

状況

こんなテスト対象コードがありました。

class Target(object):
    def target_method(self):
        self.can_print = False
        # この中でself.can_printを更新しているが、戻り値は何もない
        self.validate()
        if self.can_print:
            return 'OK'
        return 'NG'

    def validate(self):
        is_ok = False
        # 複雑な処理の結果、is_okの値を変えている
        if is_ok:
            self.can_print = True

 
このコードの対してテストを書きますが、

  • 属性self.can_printは、メソッド内で初期化しているため、外部からデータを与えられない
  • validate()メソッドは、内部でデータベースなどで複雑な処理をしているため、is_ok=Trueとなるデータを用意できない

のため、validate()メソッドをモックに差し替えたいと考えています。

 
そこで、

class Test_Target(object):
    def test_can_not_patch(self):
        def validate_mock(self):
            self.can_print = True

        with patch.object(Target, 'validate', side_effect=validate_mock):
            sut = Target()
            actual = sut.target_method()
            assert actual == 'OK'

と、patch.object()の引数side_effectを使って、validateメソッドをvalidate_mockメソッドに差し替えようと考えました。

しかし、これではvalidate_mock()の引数の数が合わず、テストを実行するとエラーになります。

# > ret_val = effect(*args, **kwargs)
# E TypeError: validate_mock() missing 1 required positional argument: 'self'

 

対応

unittest.mock.patch.object()の引数autospecを使います。

autospecは、  

autospec は mock の API を元のオブジェクト (spec) に制限しますが、再帰的に適用される (lazy に実装されている) ので、 mock の属性も spec の属性と同じ API だけを持つようになります。さらに、 mock された関数/メソッドは元と同じシグネチャを持ち、正しくない引数で呼び出されると TypeError を発生させます。

(中略)

patch() か patch.object() に autospec=True を渡すか、 create_autospec() 関数を使って spec をもとに mock を作ることができます。 patch() の引数に autospec=True を渡した場合、置換対象のオブジェクトが spec オブジェクトとして利用されます。 spec は遅延処理される (mock の属性にアクセスされた時に spec が生成される) ので、非常に複雑だったり深くネストしたオブジェクト (例えばモジュールをインポートするモジュールをインポートするモジュール) に対しても大きなパフォーマンスの問題なしに autospec を使うことができます。

26.5.5.8. autospec を使う | 26.5. unittest.mock — モックオブジェクトライブラリ — Python 3.6.0 ドキュメント

とある通り、autospec=Trueとすることで、mockの属性と対象オブジェクト(Target.validate())の属性が一致します。

 
これで引数selfが使えるため、オブジェクトの属性self.can_printを更新できます。

class Test_Target(object):
    def test_can_patch(self):
        def validate_mock(self):
            self.can_print = True

        with patch.object(Target, 'validate', autospec=True, side_effect=validate_mock):
            sut = Target()
            actual = sut.target_method()
            assert actual == 'OK'

 
テストもpassしました。

 

ソースコード

GitHubにあげました。e.g._set_self_attrディレクトリが今回のものです。
thinkAmi-sandbox/python_mock-sample