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
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
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
110
Colabのみで初心者向けPythonハンズオン.pdf
mizzsugar
0
91
厳しさとゆるさの間で迷う人に捧げる個人開発記
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
[GoCon2026] When Goroutines Are Not Enough: Runtime Locality in High-Throughput Go
takehaya
6
2.3k
iOSDC Japan 2026 - Swiftで作って学ぼう!データベース自作入門
kaseken
2
170
テストを司るデーモンに会いに行く 〜隔離した仮想マシンでテストを通すまで〜
h1d3mun3
1
500
Vue Fes Japan 2026 タイムテーブル徹底解説
448jp
1
250
Building an Out-of-Order CPU
latte72
1
790
新卒PdEのリアル
ryu1013
1
500
モジュールの視点からSwiftを読み解く #iosdc
s_shimotori
0
180
XP祭りでしか伝わらないフリップネタ #xpjug
murabayashi
0
150
技術的負債を組織課題として解く-増えすぎたマイクロサービスとの戦い-
reimaru
1
2k
FreeBSDでZabbixを動かす
kenkino
0
310
{ Android | Kotlin } Gradle Plugin in 2026
ryunen344
1
320
AI に Inclusive UI を書かせよう — Design Rules Skill で Compose UI を作り直す
theoriatec2024
1
500
Featured
See All Featured
The #1 spot is gone: here's how to win anyway
tamaranovitovic
4
1.2k
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
430
How Fast Is Fast Enough? [PerfNow 2025]
tammyeverts
3
890
The Illustrated Guide to Node.js - THAT Conference 2024
reverentgeek
1
510
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
240
Fireside Chat
paigeccino
43
4k
AI Search: Implications for SEO and How to Move Forward - #ShenzhenSEOConference
aleyda
1
1.4k
The Art of Programming - Codeland 2020
erikaheidi
57
14k
Balancing Empowerment & Direction
lara
6
1.3k
The Spectacular Lies of Maps
axbom
PRO
1
1k
Ecommerce SEO: The Keys for Success Now & Beyond - #SERPConf2024
aleyda
1
2.2k
Designing Experiences People Love
moore
143
24k
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
ありがとうございました!