Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
過去の私に伝えたい、 Pythonのunittest.mockのあれこれ
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
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
厳しさとゆるさの間で迷う人に捧げる個人開発記
mizzsugar
0
62
SQLModel入門〜クエリと型〜
mizzsugar
3
1.5k
フルリモート向いてないと思っていた私が、なんだかんだ健やかに 1年半フルリモート出来ている話
mizzsugar
1
160
Djangoでのプロジェクトだって型ヒントを運用出来る!
mizzsugar
4
9.1k
「動くものは作れる」の一歩先へ 〜「自走プログラマー」の紹介〜
mizzsugar
0
640
pytestの第一歩 〜「テスト駆動Python」の紹介〜
mizzsugar
3
480
データ分析ツール開発でpoetryを使う選択肢
mizzsugar
1
1.2k
unittest.mockを使ってテストを書こう
mizzsugar
5
7k
変数に変数を代入したら?
mizzsugar
1
2.7k
Other Decks in Programming
See All in Programming
ADKを使って簡単にAIエージェントを作ってみよう
k1mu21
0
280
セキュリティの専門家じゃなくてもできる。「セキュリティ意識」をアップデートして サプライチェーン攻撃への耐性を高めよう。
tk3fftk
5
920
JavaDoc 再入門
nagise
1
410
jQueryをバージョンアップする前に使いたいjQuery Migrate
matsuo_atsushi
0
580
LLM本来の能力を解き放つサンドボックス技術とAI民主化への適用
yukukotani
3
4.5k
依存関係から依存物へ―Dependencyという言葉の歴史をひも解く
j_lee
0
130
TSKaigi Night Talks 2026_TypeScriptでサプライチェーンの整合性を型に閉じ込める
geekplus_tech
0
400
ローカルLLMを使ってB2Bサービスを作っていての学び
yaotti
0
210
TypeScript+Orvalで実現する型安全かつ堅牢でスケーラブルなマルチチャネル通知基盤 / TSKaigi Night talks ~after conference~
d0riven
0
360
PHPで使える日時の表現と、その知り方 #frontend_phpcon_do
o0h
PRO
0
260
Go1.27で導入されるジェネリクスメソッドでできること
mackee
0
170
作って学ぶ、 JSX (TSX) ランタイムの基本
syumai
7
1.7k
Featured
See All Featured
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.5k
For a Future-Friendly Web
brad_frost
183
10k
コードの90%をAIが書く世界で何が待っているのか / What awaits us in a world where 90% of the code is written by AI
rkaga
62
44k
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
480
Into the Great Unknown - MozCon
thekraken
41
2.6k
Jamie Indigo - Trashchat’s Guide to Black Boxes: Technical SEO Tactics for LLMs
techseoconnect
PRO
0
180
Thoughts on Productivity
jonyablonski
76
5.2k
A Tale of Four Properties
chriscoyier
163
24k
Designing for Timeless Needs
cassininazir
1
260
svc-hook: hooking system calls on ARM64 by binary rewriting
retrage
2
310
KATA
mclloyd
PRO
35
15k
The Art of Programming - Codeland 2020
erikaheidi
57
14k
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
ありがとうございました!