Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
過去の私に伝えたい、 Pythonのunittest.mockのあれこれ
Search
mizzsugar
October 09, 2019
Programming
1.2k
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
過去の私に伝えたい、 Pythonのunittest.mockのあれこれ
mizzsugar
October 09, 2019
More Decks by mizzsugar
See All by mizzsugar
自分のPCにPython環境を用意して_VSCodeでStreamlitアプリを動かすまでの8ステップ.pdf
mizzsugar
0
130
Colabのみで初心者向けPythonハンズオン.pdf
mizzsugar
0
93
厳しさとゆるさの間で迷う人に捧げる個人開発記
mizzsugar
0
76
SQLModel入門〜クエリと型〜
mizzsugar
3
1.6k
フルリモート向いてないと思っていた私が、なんだかんだ健やかに 1年半フルリモート出来ている話
mizzsugar
1
170
Djangoでのプロジェクトだって型ヒントを運用出来る!
mizzsugar
4
9.2k
「動くものは作れる」の一歩先へ 〜「自走プログラマー」の紹介〜
mizzsugar
0
660
pytestの第一歩 〜「テスト駆動Python」の紹介〜
mizzsugar
3
490
データ分析ツール開発でpoetryを使う選択肢
mizzsugar
1
1.2k
Other Decks in Programming
See All in Programming
Unity6 URPによる海底をテーマにしたVJシステムの解説 | Unity Shader 完全に理解した 勉強会
gam0022
0
240
更なる可用性を求めて、5年間運用したKotlinのアプリケーションをGoでリプレイスする話
ken_tunc
0
420
Verilogで学ぶCPU自作入門.pdf
uyuki234
7
3.9k
GemmaをJevのように使ってみる / Use Gemma like Jev
kishida
5
460
Webの地図
yosuke_furukawa
PRO
6
5k
Embedded Swiftで作る自作USBデバイス によるiOSデバイスの自動テスト / iOSDC Japan 2026 glassfiber
glassfiber
0
250
Apple Intelligence を用いた個人情報誤送信防止、及びユーザーリクエスト体験の改善について
yukiny
0
260
AWSに止められる覚悟してますか?
morizo_1984
2
490
モジュールの視点からSwiftを読み解く #iosdc
s_shimotori
0
290
標準パッケージに uuid が追加された 背景から見る Go らしい意思決定 / go_127_uuid_decision
convto
5
9.3k
FreeBSDでZabbixを動かす
kenkino
0
340
SREの越境 / SRE Collaboration
y0hgi
2
290
Featured
See All Featured
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6.3k
How to Build an AI Search Optimization Roadmap - Criteria and Steps to Take #SEOIRL
aleyda
1
2.2k
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
840
A Soul's Torment
seathinner
8
3.7k
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
1
570
Typedesign – Prime Four
hannesfritz
42
3.2k
Code Review Best Practice
trishagee
74
20k
Hiding What from Whom? A Critical Review of the History of Programming languages for Music
tomoyanonymous
3
1.3k
Ethics towards AI in product and experience design
skipperchong
2
410
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.9k
Testing 201, or: Great Expectations
jmmastey
46
8.3k
Producing Creativity
orderedlist
PRO
348
41k
Transcript
過去の私に伝えたい、 Pythonのunittest.mockの あれこれ 2019/10/09 みんなのPython勉強会 @mizzsugar0425
お前、誰よ • Twitter : @mizzsugar0425 • PythonでWeb開発しています。 ◦ 仕事:Django, Vue.js,
MySQL ◦ 趣味:Pyramid, Nuxt.js, TypeScript, PostgreSQL • Djangogirlsコーチやってます。先日、翻訳デビューしました! • コーヒーと自転車が好きです。
前提 • unittest.mock特有の話ではないので、pytestでも使えます。 • 単体テストのことを話します。E2Eテストやシステムテストや結合テストについては 話しません。 • テスト駆動開発については話しません。 • スライド内に収めるためにサンプルコードがPEP8に反していることがありますがご
了承ください。 • 議論は大歓迎です! ただし、強い言葉や否定の言葉のない優しい世界でお願い します(>人<)
単体テストって? • 単体テストの目的 ◦ プロダクトコードが意図した通りに動くことを確認する • 単体テストを書いたら嬉しいこと ◦ いつでも同じテストができるので不安なくリファクタリングできる ◦
振る舞いを変更した場合に正しく振る舞えるかすばやく確認できる ◦ テストを書くことでテスト対象となる関数やライブラリの最初のユーザーとなる。それによって それら が使いやすいかを判断できる。
こういう時に単体テスト書いていると嬉しい calculate.py 飲食店で注文した商品の合計に消費税を加えて購入金額を計算する関数です。 この関数では金額の合計に 1.08をかけるだけですが・・・ ※今回の例はわかりやすさを重視して厳密な軽減税率のルールには則っていません。 import dataclasses from typing
import Iterable @dataclasses.dataclass(frozen=True) class Item: name: str price: int def price(items: Iterable[Item]) -> int: # 簡単にするためにひとまず int()で端数処理します return int(sum(item.price for item in items) * 1.08)
軽減税率が導入されるようになった calculate.py import dataclasses from typing import Iterable @dataclasses.dataclass(frozen=True) class
Item: name: str price: int def price(items: Iterable[Item], eat_in: bool) -> int: if eat_in: return int(sum(item.price for item in items) * 1.1) return int(sum(item.price for item in items) * 1.08)
キャッシュレスなら5%減額されるようになった calculate.py この後に更にルールの追加、変更・・・ となると 手動で関数の振る舞いが正しいかを確認するのが大変です。 import dataclasses from typing import Iterable
@dataclasses.dataclass(frozen=True) class Item: name: str price: int def price(items: Iterable[Item], eat_in: bool, cache_less: bool) -> int: tax_rate = 1.10 if eat_in else 1.08 reduction = 0.95 if cache_less else 1 return int(sum(item.price for item in items) * tax_rate * reduction)
単体テストが書いていると import unittest import src.calculate class TestCalculate(unittest.TestCase): def setUp(self): self.item_1
= src.calculate.Item(name='sample_1', price=500) self.item_2 = src.calculate.Item(name='sample_2', price=1000) def test_calculate_sum(self): items = [self.item_1, self.item_2] actual = src.calculate.price(items) expected = 1620 self.assertEqual(expected, actual)
軽減税率導入に伴い、テストケース見直し import unittest import src.calculate class TestCalculate(unittest.TestCase): """中略""" def test_calculate_sum_eatin(self):
items = [self.item_1, self.item_2] actual = src.calculate.price(items, eat_in=False) expected = 1620 self.assertEqual(expected, actual) def test_calculate_takeout(self): items = [self.item_1, self.item_2] actual = src.calculate.price(items, eat_in=True) expected = 1650 self.assertEqual(expected, actual) 振る舞いの変更 に応じて テストメソッド名 も変更 イートインの場合と テイクアウトの場合の テストケースを作成
キャッシュレス導入に伴い、テストケース見直し class TestCalculate(unittest.TestCase): """中略""" def test_calculate_sum_eatin_cache(self): items = [self.item_1, self.item_2]
actual = src.calculate.price(items, eat_in=False, cache_less=False) expected = 1620 self.assertEqual(expected, actual) def test_calculate_takeout_cache(self): items = [self.item_1, self.item_2] actual = src.calculate.price(items, eat_in=True, cache_less=False) expected = 1650 self.assertEqual(expected, actual) def test_calculate_sum_eatin_cache_less(self): items = [self.item_1, self.item_2] actual = src.calculate.price(items, eat_in=False, cache_less=True) expected = 1539 self.assertEqual(expected, actual) def test_calculate_takeout_cache_less(self): items = [self.item_1, self.item_2] actual = src.calculate.price(items, eat_in=True, cache_less=True) expected = 1567 self.assertEqual(expected, actual)
単体テストがないと・・・ • 変更があるたびに手動でテストしないといけない • プロダクトコードのみだと何を渡した時に何が返ってくるか第三者に分かりづらい 単体テストがあると・・・ • テストを実行するためのコマンドだけで正しく動いているか確認できる • テストケースの内容をみるとどんな条件でどのように動くか確認できる
• レビュー時にテストケースをみることで確認すべき観点の過不足を確認できる 単体テストがあると便利
unittest.mockとは • Pythonでテストする際に利用するPythonの標準ライブラリ。プロダクトコードの一部 をモックオブジェクトに置き換え、作成した関数やモジュールの振る舞いに関するア サーションメソッドを実行できる • モックオブジェクトに置き換えることで構築に手間がかかるオブジェクトを高速に利 用できる • 「mock」という言葉には「まねる」「まがいの」という意味があります。他のオブジェク
トや関数のふりをした偽物として振る舞うイメージをもっていただければ ※本発表では、プロダクトコードの一部を置き換えたオブジェクトを「モックオブジェ クト」と呼び、置き換えることを「モックする」と呼びます。スタブとモックという概念が ありますが、時間の都合上言及しないので厳密には分けません。
例えばこんな関数をテストしたいとします kuji.py import random def kuji() -> str: fortune_number =
random.randrange(10) if fortune_number % 2 == 0: return 'あたり' return 'はずれ'
import unittest import src.kuji class TestKuji(unittest.TestCase): def test_win_if_multiple_of_2(self): actual =
src.kuji.kuji() expected = 'あたり' self.assertEqual(expected, actual) def test_fail(self, mock_random_number): actual = src.kuji.kuji() expected = 'はずれ' self.assertEqual(expected, actual) テストが通る時と通らない時がある事件発生 これじゃあ このテストが通れば安心と自 信をもちきれない!!
import unittest import src.kuji class TestKuji(unittest.TestCase): def test_win_if_multiple_of_2(self): actual =
src.kuji.kuji() expected = 'あたり' self.assertEqual(expected, actual) def test_lose(self, mock_random_number): actual = src.kuji.kuji() expected = 'はずれ' self.assertEqual(expected, actual) random.randrange(10)が実行さ れているため、 test_win_if_multiple_of_2で2で 割り切れない数字が利用されるこ ともある。 同じ考え方で、test_loseで2で割り 切れる数が利用され テストが通らないことも。 なぜ不安定なテストになってしまったのか
モックオブジェクトで安定したテストに class TestKuji(unittest.TestCase): @unittest.mock.patch('random.randrange') def test_win_if_multiple_of_2(self, mock_random_number): mock_random_number.return_value = 2
actual = src.kuji.kuji() expected = 'あたり' self.assertEqual(expected, actual) @unittest.mock.patch('random.randrange') def test_lose(self, mock_random_number): mock_random_number.return_value = 1 actual = src.kuji.kuji() expected = 'はずれ' self.assertEqual(expected, actual) テスト毎にランダムに値が 出力される random.randrange()を モックします return_valueで random.randrange()が 返す値を2に固定します このテストメソッドでは常に 2が返され、安定したテス トになります。
も〜っと!モックオブジェクト • テストを実行する時間やタイムゾーンにテスト結果が左右されない • データベースアクセスの箇所を置き換えることで、データベースの内容にテスト結果 が左右されない • ・外部APIを利用する箇所を置き換えることで、ネットワークにテスト結果が左右され ない。また、テストのために何回もリクエスト送って申し訳ないのがなくなる ->
テストが失敗する外的要因を排除できる -> 処理内容の変更やリファクタリングを安心して行える • とはいえ、モックオブジェクトに置き換えた箇所の動きは保証されていないのでよく テストされたものを使うなど注意が必要
モックオブジェクトドッカ〜ン!とならないために 1. datetime.datetime.nowをモックできない問題 2. 外部APIを利用したテストをどうかけばいいのかわからない問題
datetime.datetime.now()をモックできない問題 import datetime def greet() -> str: now = datetime.datetime.now()
if 5 <= now.hour < 12: return 'おはようございます' elif 12 <= now.hour < 18: return 'こんにちは' return 'こんばんは' https://t-wada.hatenablog.jp/entry/design-for-testability をPythonに書き換え greet.py
import datetime import unittest.mock import pytz import src.greet @unittest.mock.patch('datetime.datetime.now') def
test_morning(self, mock_datetime): mock_datetime.return_value = datetime.datetime( 2019, 10, 1, 5, 0, 0, 0 ) expected = 'おはようございます' self.assertEqual(expected, src.greet.greet()) -> エラーになってテストできない test_greet.py ※CPythonで書かれたモジュールのオブジェクトが Immutableであることが原因です。 PyPy3.5で動作確認したところ、正常に動きました
案1: ライブラリ「freezegun」を使う from freezegun import freeze_time @freeze_time('2019-10-01 05:00:00') def test_morning(self):
expected = 'おはようございます' self.assertEqual( expected, src.greet.greet() ) test_greet.py
案2: datetime.datetime.now()を引数にもつ class TestGreet(unittest.TestCase): def test_morning(self): expected = 'おはようございます' self.assertEqual(
expected, src.greet.greet( datetime.datetime(2019, 10, 1, 5, 0, 0, 0) ) ) テストはこんな感じになって・・・ test_greet.py
def greet(now: datetime.datetime) -> str: if 5 <= now.hour <
12: return 'おはようございます' elif 12 <= now.hour < 18: return 'こんにちは' return 'こんばんは' 時間を引数にもたせると datetime.datetime.now()ではなく指定した日時で使いたい! という要望が出た時にも再利用できます。
外部APIを利用した関数のテストが書けない 日本の天気を取得するAPIを利用してピクニックの実施を判定する関数をテストするとし ます。 https://tenki.example.com/today/[郵便番号(7桁の半角数字ハイフンなし)] をGETでリクエストを送ると下記のようなJSONが返されます。 { '郵便番号': '1050004' '都道府県': '東京都',
'市区町村': '港区新橋', '天気': '晴れ' } 存在しない郵便番号を指定してリクエストが送られたらHTTPステータスコードが404でレ スポンスが返されます。 ※これは架空のAPIであり、実際には使用できません
テスト対象の関数 import http import requests def picnic(postal_code: str) -> str:
request_url = f'https://tenki.example.com/today/{postal_code}' response = requests.get(request_url) if response.status_code == http.HTTPStatus.NOT_FOUND: raise ValueError if response.json().get('天気') == '晴れ': return 'ピクニックは決行' return 'ピクニックは延期' ピクニックの行き先の郵便番号を引数に入力します。 晴れならばピクニックは決行、それ以外ならば延期とします。
requests.getをモックしてネットワーク通信しない import unittest import unittest.mock import src.tenki class TestTenki(unittest.TestCase): @unittest.mock.patch('requests.get')
def test_go_on_a_picnic_if_sunny(self, mock_request): mock_request.return_value = unittest.mock.Mock( status_code=200 ) mock_request.return_value.json.return_value = { '天気': '晴れ' } actual = src.tenki.picnic(1050004) expected = 'ピクニックは決行' self.assertEqual(expected, actual) 天気しか利用しないので他の 項目は返していませんが、 入れても問題ありません GET以外の HTTPリクエストでも 同様にモックしてください
まとめ • 単体テストを書くとリファクタリングや変更が安心 • テストを高速に安定して実行するためにunittest.mockモジュールを使おう • モックオブジェクトに置き換えた箇所の動きは保証されていないのでよくテストされ たものを使うなど注意が必要 • 3rdパーティライブラリでモックできない問題を解決
• オブジェクトを引数に渡すなどプロダクトコードの設計を見直す解決も • モックオブジェクトで外部APIを利用した関数も怖くない
参考文献 • テスト駆動開発 https://www.amazon.co.jp/gp/product/B077D2L69C/ref=dbs_a_def_rwt_hsch_vapi_tkin_p1_i0 • 実践テスト駆動開発 https://www.amazon.co.jp/%E5%AE%9F%E8%B7%B5%E3%83%86%E3%82%B9%E3%83%88%E9%A7 %86%E5%8B%95%E9%96%8B%E7%99%BA-Object-Oriented-SELECTION-Freeman/dp/4798124583/r ef=sr_1_1?qid=1570540448&refinements=p_27%3ASteve+Freeman&s=books&sr=1-1&text=Steve+F reeman
• Pylons 単体テストガイドラインhttp://docs.pylonsproject.jp/en/latest/community/testing.html • 現在時刻が関わるユニットテストから、テスト容易性設計を学ぶ https://t-wada.hatenablog.jp/entry/design-for-testability
ありがとうございました!