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
·
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
厳しさとゆるさの間で迷う人に捧げる個人開発記
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
Agentic UI
manfredsteyer
PRO
0
190
気圧・高度・GPSを記録&可視化するアプリ「Koudo」を作った話
hjmkth
1
320
Oxcを導入して開発体験が向上した話
yug1224
4
340
Spec Driven Development | AI Summit Lisbon
danielsogl
PRO
0
210
Contextとはなにか
chiroruxx
1
370
生成AI時代にこそ効くGo | Why Go Works in the Age of Generative AI
mom0tomo
8
3.3k
Language Server 使ってる? 〜VSCode と Zed の場合〜 / Are you using a Language Server? ~For VS Code and Zed~
handlename
0
800
軽量Java基盤の設計 DIコンテナに頼らない、長期保守と1秒起動の実現 JJUG CCC 2026 Spring
macha64
0
570
Strategic Design in the Frontend: Moduliths & Micro Frontends @DDDEurope
manfredsteyer
PRO
0
130
Creating Composable Callables in Contemporary C++
rollbear
0
160
エージェンティックRAGにAWSで入門しよう!
har1101
9
1.7k
フロントエンドとバックエンドで「1文字」を揃えよう
youkidearitai
PRO
0
740
Featured
See All Featured
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
190
Navigating Team Friction
lara
192
16k
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
170
Thoughts on Productivity
jonyablonski
76
5.2k
Fantastic passwords and where to find them - at NoRuKo
philnash
52
3.7k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
3.5k
Designing for humans not robots
tammielis
254
26k
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Why Mistakes Are the Best Teachers: Turning Failure into a Pathway for Growth
auna
0
170
Building Experiences: Design Systems, User Experience, and Full Site Editing
marktimemedia
0
540
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
11k
Building AI with AI
inesmontani
PRO
1
1.1k
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
ありがとうございました!