この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、みかみです。
そろそろ梅の良い季節v(今週末は、湯島天神梅まつり?!
はじめに
やりたいこと
- Web APIからレスポンスを取得する Python コードのテストをしたい
- HTTP接続できない環境でもテストできるようにしたい
- Python で mock を使ってみたい!
動作環境
- Windows10(Mac VMware Fusion)
- Python 3.6.0(unittest を pip install 済み)
やってみた
Web APIからレスポンスを取得する関数のテスト
お天気APIで、東京のお天気を取得するコードです。
Python の HTTPライブラリ requests でGETリクエストを投げてレスポンスを取得しています。
get_weather.py
import requests
def get_resp(url):
resp = requests.get(url)
if resp.status_code != 200:
print('Get Data Failed...(error code : {})'.format(resp.status_code))
raise
return resp.json()
def main():
url = 'http://weather.livedoor.com/forecast/webservice/json/v1?city=130010'
actual = get_resp(url)
print(actual)
if __name__ == '__main__':
main()
実行すると、APIの response を print します。
C:\Users\mikami.yuki\work\apiTest>py get_weather.py
{'pinpointLocations': [{'link': 'http://weather.livedoor.com/area/forecast/1310100', 'name': '千代田区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310200', 'name': '中央区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310300', 'name': '港区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310400', 'name': '新宿区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310500', 'name': '文京区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310600', 'name': '台東区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310700', 'name': '墨田区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310800', 'name': '江東区'}, {'link': 'http://weather.livedoor.com/area/forecast/1310900', 'name': '品川区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311000', 'name': '目黒区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311100', 'name': '大田区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311200', 'name': '世田谷区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311300', 'name': '渋谷区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311400', 'name': '中野区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311500', 'name': '杉並区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311600', 'name': '豊島区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311700', 'name': '北区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311800', 'name': '荒川区'}, {'link': 'http://weather.livedoor.com/area/forecast/1311900', 'name': '板橋区'}, {'link': 'http://weather.livedoor.com/area/forecast/1312000', 'name': '練馬区'}, {'link': 'http://weather.livedoor.com/area/forecast/1312100', 'name': '足立区'}, {'link': 'http://weather.livedoor.com/area/forecast/1312200', 'name': '葛飾区'}, {'link': 'http://weather.livedoor.com/area/forecast/1312300', 'name': '江戸川区'}, {'link': 'http://weather.livedoor.com/area/forecast/1320100', 'name': '八王子市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320200', 'name': '立川市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320300', 'name': '武蔵野市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320400', 'name': '三鷹市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320500', 'name': ' 青梅市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320600', 'name': '府中市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320700', 'name': '昭島市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320800', 'name': '調布市'}, {'link': 'http://weather.livedoor.com/area/forecast/1320900', 'name': '町田市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321000', 'name': '小金井市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321100', 'name': '小平市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321200', 'name': '日 野市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321300', 'name': '東村山市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321400', 'name': '国分寺市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321500', 'name': '国立市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321800', 'name': '福生市'}, {'link': 'http://weather.livedoor.com/area/forecast/1321900', 'name': '狛江市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322000', 'name': '東大和市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322100', 'name': ' 清瀬市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322200', 'name': '東久留米市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322300', 'name': '武蔵村山市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322400', 'name': '多摩市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322500', 'name': '稲城市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322700', 'name': '羽村市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322800', 'name': 'あきる野市'}, {'link': 'http://weather.livedoor.com/area/forecast/1322900', 'name': '西東京市'}, {'link': 'http://weather.livedoor.com/area/forecast/1330300', 'name': '瑞穂町'}, {'link': 'http://weather.livedoor.com/area/forecast/1330500', 'name': '日の出町'}, {'link': 'http://weather.livedoor.com/area/forecast/1330700', 'name': '檜原村'}, {'link': 'http://weather.livedoor.com/area/forecast/1330800', 'name': '奥多摩町'}], 'link': 'http://weather.livedoor.com/area/forecast/130010', 'forecasts': [{'dateLabel': '今日', 'telop': '晴のち曇', 'date': '2017-02-08', 'temperature': {'min': None, 'max': None}, 'image': {'width': 50, 'url': 'http://weather.livedoor.com/img/icon/5.gif', 'title': '晴のち曇', 'height': 31}}, {'dateLabel': '明日', 'telop': '曇時々雨', 'date': '2017-02-09', 'temperature': {'min': {'celsius': '2', 'fahrenheit': '35.6'}, 'max': {'celsius': '5', 'fahrenheit': '41.0'}}, 'image': {'width': 50, 'url': 'http://weather.livedoor.com/img/icon/10.gif', 'title': '曇時々雨', 'height': 31}}, {'dateLabel': '明後日', 'telop': '曇時々晴', 'date': '2017-02-10', 'temperature': {'min': None, 'max': None}, 'image': {'width': 50, 'url': 'http://weather.livedoor.com/img/icon/9.gif', 'title': '曇時々晴', 'height': 31}}], 'location': {'city': '東京', 'area': '関東', 'prefecture': '東京都'}, 'publicTime': '2017-02-08T17:00:00+0900', 'copyright': {'provider': [{'link': 'http://tenki.jp/', 'name': '日本気象協会'}], 'link': 'http://weather.livedoor.com/', 'title': '(C) LINE Corporation', 'image': {'width': 118, 'link': 'http://weather.livedoor.com/', 'url': 'http://weather.livedoor.com/img/cmn/livedoor.gif', 'title': 'livedoor 天気情 報', 'height': 26}}, 'title': '東京都 東京 の天気', 'description': {'text': ' 日本付近は、冬型の気圧配置が緩んできました。\n\n【関東甲信地方】\n 関東甲信地方は、曇りとなっています。\n\n 8日は、気圧の谷の影響で曇りとなり、雨や雪の降る所があるでしょう。\n\n 9日は、低気圧が本州南岸を東に 進むため、曇りで時々雨や雪が降るでし\nょう。伊豆諸島では、雷を伴う所がある見込みです。\n\n 関東近海では、8日はうねりを伴い波が高いでしょう。9日はしける所が\nある見込みです。船舶は高波に注意してください。\n\n【東京地方】\n 8日は、曇りとなるでしょう。\n 9日は、曇り時々雨か雪となる見込み です。', 'publicTime': '2017-02-08T22:02:00+0900'}}
この requests lib をmock化したい!
テストコードを書きます。
test_get_weather.py
from get_weather import get_resp
import unittest
from unittest import mock
# requests lib の mock
def mocked_requests_get(*args, **kwargs):
class MockResponse:
def __init__(self, json_data, status_code):
self.json_data = json_data
self.status_code = status_code
def json(self):
return self.json_data
if args[0] == 'url_valid':
return MockResponse({"key1": "value1"}, 200)
return MockResponse({}, 404)
# テスト用クラス
class TestGetResp(unittest.TestCase):
# 正常系確認テストケース
@mock.patch('requests.get', side_effect=mocked_requests_get)
def test_get_resp_ok(self, mock_get):
json_data = get_resp('url_valid')
self.assertEqual(json_data, {"key1": "value1"})
# 異常系確認テストケース
@mock.patch('requests.get', side_effect=mocked_requests_get)
def test_get_resp_ng(self, mock_get):
with self.assertRaises(RuntimeError):
json_data = get_resp('url_invalid')
if __name__ == '__main__':
unittest.main()
実行してみます。
C:\Users\mikami.yuki\work\apiTest>py test_get_weather.py
Get Data Failed...(error code : 404)
..
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
requests.get() をmock化できました!
mockをもっとシンプルに使ってみる
例えば、
- main() から hoge() をコール
- hoge() が fuga() をコールして、fuga() の戻り値を main() に返す
- main() で戻り値をprint
なコードがあるとします↓
test_mock.py
def hoge():
return fuga()
def fuga():
return 'Here is Fuga!'
def main():
res = hoge()
print(res)
if __name__ == '__main__':
main()
実行してみると
C:\Users\mikami.yuki\work\apiTest>py test_mock.py
Here is Fuga!
ちゃんと、fuga() の戻り値をprintしてくれます。
この hoge() のユニットテストをしたいので、テストコードから fuga() をコールします。
が、 fuga() とは分離したいので、fuga() を mock に置き換えてみます↓
test_mock.py
from unittest import mock
def hoge():
return fuga()
def fuga():
return 'Here is Fuga!'
# fuga() を patch(mock化)
@mock.patch('__main__.fuga')
def test_hoge(mock_fuga):
# mock の戻り値を設定
mock_fuga.return_value = 'Here is Mock!'
res = hoge()
print(res)
if __name__ == '__main__':
test_hoge()
たったこれだけ!
実行してみます。
C:\Users\mikami.yuki\work\apiTest>py test_mock.py
Here is Mock!
ちゃんと mock が呼ばれました!
おわりに(所感)
なに、これ、すごい!(めっさらくちん@@v
- Java → Junit → Eclipse で Plugin がどうとか、アノテーションがどうとか。。(あせ
- PHP@Laravel + Mockery → UnitTestのこと考えて初めからファサードクラス実装したり、composer で Mockery 入れたり、めんどく(ry
→ Python の unittestってすぐれものv
※個人的感想すみませんmm(近頃 Python 信者なもので。。