[python 3]mockの1セット勝負-2


第1部長編物語の最後に
お願いだgetメソッドにパッチを適用し、'blah blah'という文字列を返します.
でも私たちは、
1.Response個のオブジェクトを返したい!
2.getposturlまたはheaderにより、多種多様なResponseを返却したい!
応答を返す
1番はあまり難しくありません.Response対象を作ればいい!どうしよう.
Custom Response
class Response:
    def __init__(self, **kwargs):
        self.status_code = kwargs['status']
        self.data = kwargs['data']

    def json(self):
        return self.data

res_data = {
    'message': 'hello, response',
}
res = Response(status=200, data=res_data)
res.status_code
> 200

res_json = res.json()
res_json['message']
> 'hello, response'
筆者はすべてのResponseにおいてstatus_code,json()語樹状図のみを用いた.
したがって、少なくとも2つの機能を有するCustom Responseオブジェクトが生成される.
このCustom Responseを直接適用しましょう!
# test.py

from unittest import mock
from rest_framework.test import APITestCase

class Response:
    def __init__(self, **kwargs):
        self.status_code = kwargs['status']
        self.data = kwargs['data']

    def json(self):
        return self.data

@mock.patch("hello_world.requests.get")
class TestAPIHello(APITestCase):
    def setUp(self):
    	self.user = get_user()
    	do_stuff()
       
    def testcase_00_hello_world(self, mock_get):
        mock_get.return_value = Response(
            status=200,
            data={'user': 'Lee'}
        )
    	response = self.client.get(url="/api/hello-world/")
        self.assertEqual(response.status_code, 200)
こつこつ!
実際の応答として返すrequestsモジュールのResponseを返却したい場合は、次のようにできます.
# test.py

import requests
from unittest import mock
from rest_framework.test import APITestCase

@mock.patch("hello_world.requests.get")
class TestAPIHello(APITestCase):
    def setUp(self):
    	self.user = get_user()
    	do_stuff()
       
    def testcase_00_hello_world(self, mock_get):
        my_response = requests.models.Response()
        response.status_code = 200
        response_content = json.dumps({'user': 'Lee'})
        response._content = str.encode(response_content)
        
        mock_get.return_value = my_response
    	response = self.client.get(url="/api/hello-world/")
        self.assertEqual(response.status_code, 200)
testcase_00_hello_world手法では,Responseモデルを実際に用いた.
残念なことに、contentというprotectedプロパティに直接アクセスする必要があります.
複数の応答を返す
しかし、すべてのrequests.get()メソッドが同じResponseを送信することは望ましくない.
もちろん四半期によって例外もたくさんあります.
これらのブランチと例外処理のすべてをテストコードで上書きするには、次の手順に従います.
場合によっては多様なResponseが発生するはずです!
この時は私が使う機能もあります
side_effect
Mockのside_effectは、シミュレーション例において예외の異常を発生させることを可能にする.
作成した함수をmockインスタンスに適用できます.
正式な書類は、side_effectを使用する様々な方法を説明する.
簡単なサンプルコードを次に示します.
from unittest import mock

def hello_world(name):
    return f'Hello, world and {name}'

m = mock.Mock()
m.side_effect = hello_world
m('Lee !!')
> Hello, world and Lee !!
行け!
サンプルコード
複数url
テストされたコードとテストコードをよく見てください.
# hello_world.py

import requests
from rest_framework.views import APIView
from rest_framework.response import Response

class HelloView(APIView):
    def get(self, request):
        res1 = requests.get(url='https://some.com/api/user-info/')
        
        # Send one more request !!
        res2 = requests.get(url='https://some.com/api/org-info/')
        
        if res1.status_code == res2.status_code == 200:
            res1_json = res1.json()
            res2_json = res2.json()
            data = {
                "message": "Hello, World!",
                "user": res1_json["user"],
                "org": rest2_json["org"],
            }
            return Response(data, status=200)
        
        data = {
            "message": "Error!",
            "user": "No user",
        }
        return Response(data, status=503)
2つのurlにrequestが送信されます.
# test.py

import requests
from unittest import mock
from rest_framework.test import APITestCase

def mock_request_get(**kwargs):
    response = requests.models.Response()
    response.status_code = 200
    url = kwargs.get('url')
    
    if url == 'https://some.com/api/user-info/':
        response_content = json.dumps({'user': 'Lee'})
        response._content = str.encode(response_content)
        
    elif url == 'https://some.com/api/org-info/':
        response_content = json.dumps({'org': 'Avengers'})
        response._content = str.encode(response_content)
        
    else:
        response.status_code = 400
    return response

@mock.patch("hello_world.requests.get")
class TestAPIHello(APITestCase):
    def setUp(self):
    	self.user = get_user()
    	do_stuff()
       
    def testcase_00_hello_world(self, mock_get):      
        mock_get.side_effect = mock_request_get
    	response = self.client.get(url="/api/hello-world/")
        self.assertEqual(response.status_code, 200)
異常が発生する
外部apiサーバにリクエストを送信中にエラーが発生したと仮定した場合は、次のコードも実行できます.
# hello_world.py

import requests
from rest_framework.views import APIView
from rest_framework.response import Response

class HelloView(APIView):
    def get(self, request):
        try:
            res1 = requests.get(url='https://some.com/api/user-info/')
            res2 = requests.get(url='https://some.com/api/org-info/')
           
        # Return response with status code 500
        # if OSError raises.
        except OSError:
            return Response(status=500)
            
        if res1.status_code == res2.status_code == 200:
            res1_json = res1.json()
            res2_json = res2.json()
            data = {
                "message": "Hello, World!",
                "user": res1_json["user"],
                "org": rest2_json["org"],
            }
            return Response(data, status=200)
        
        data = {
            "message": "Error!",
            "user": "No user",
        }
        return Response(data, status=503)
hello_world.pyからrequestsと書かれた部分にエラーハンドルコードが追加されました.
# test.py

import requests
from unittest import mock
from rest_framework.test import APITestCase

def mock_request_get(**kwargs):
    response = requests.models.Response()
    response.status_code = 200
    url = kwargs.get('url')
    
    if url == 'https://some.com/api/user-info/':
        response_content = json.dumps({'user': 'Lee'})
        response._content = str.encode(response_content)
        
    elif url == 'https://some.com/api/org-info/':
        response_content = json.dumps({'org': 'Avengers'})
        response._content = str.encode(response_content)
        
    else:
        response.status_code = 400
    return response

@mock.patch("hello_world.requests.get")
class TestAPIHello(APITestCase):
    def setUp(self):
    	self.user = get_user()
    	do_stuff()
       
    def testcase_00_hello_world(self, mock_get):      
        mock_get.side_effect = mock_request_get
    	response = self.client.get(url="/api/hello-world/")
        self.assertEqual(response.status_code, 200)
    
    def testcase_01_external_api_error(self, mock_get):
        mock_get.side_effect = OSError()
        response = self.client.get(url="/api/hello-world/")
        self.assertEqual(response.status_code, 500)
testcase_01_external_api_error試験方法を追加した.
ここではside_effectに関数を割り当てるのではなく,直接例外を割り当てる.
の最後の部分
長い文章を書くのは初めてです.力が抜けた...こんなに長く書くつもりはなかったのに、なぜかたくさん書いてしまった.
私が経験した過程を書く文章だったが、下品なコードを主とする文章になった.ガチャガチャ音を立てる
実はmockを応用する時に多くの悩みがあります
すべてのtestメソッドは、それぞれのside effectに割り当てられた関数を作成する必要がありますか?
1つのAPIモジュールにおいて、requestsに書き込まれたすべての部分は、side_effectに割り当てられた関数を配布することによって簡略化する必要があるかどうか.
このような悩みがあるのは、私が作成したAPIモジュールが1つのリクエストを処理するときに、少なくとも2、3回外部APIサーバにリクエストを送信するからです...
従ってside_effect関数からurlによって異なる処理のための分岐ゲート地獄
sidefectに直接異常発生コードを追加しますか?それとも私が作成した関数にブランチに基づいて異常を発生しますか?
mockをテストコードに最善の方法で適用しましたが、まだ満足できません.テストコードが簡潔ではないからですか?
それとも私が作ったAPIが最初からあまりよくなかったからですか?
いろいろな悩みを頭の後ろに置いて、先に書いて、それからこのように書きました.
この文章を読んでいる他の人に少し役に立つと、満足します.
ここまでです.