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
unittest.mockを使ってテストを書こう ~モックオブジェクトを使ってより単体テストの...
Search
mizzsugar
February 29, 2020
Programming
2.2k
4
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
unittest.mockを使ってテストを書こう ~モックオブジェクトを使ってより単体テストの目的に沿ったテストに~
mizzsugar
February 29, 2020
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
Developing with AI Agents — Codex, Claude Code & Cowork Practical Guide
x5gtrn
PRO
0
1.3k
PHPで使える日時の表現と、その知り方 #frontend_phpcon_do
o0h
PRO
0
260
技術的負債解消で開発者の未来を開く- AIの力でコード刷新
kmd2kmd
0
120
生成AI時代にこそ効くGo | Why Go Works in the Age of Generative AI
mom0tomo
8
3.3k
RTSPクライアントを自作してみた話
simotin13
0
630
AI 時代のソフトウェア設計の学び方
masuda220
PRO
29
13k
Vite+ Unified Toolchain for the Web
naokihaba
0
340
ADKを使って簡単にAIエージェントを作ってみよう
k1mu21
0
280
LaravelLive Japan の裏方のすべて — 第188回 PHP勉強会@東京 (2026-06-24)
suguruooki
2
120
「なぜそう決めたのか」を残し続ける仕組み ― Notion AI カスタムエージェント × Slack連携による設計判断の自動記録 - NIKKEI Tech Talk #47
niftycorp
PRO
0
230
Semantic Version 単位で戦略を柔軟に変えて、パッケージアップデートを自動化する
daitasu
1
300
Dataformのリポジトリを立ち上げるときにまずやること / dataform-day0-2026
snhryt
0
180
Featured
See All Featured
Gemini Prompt Engineering: Practical Techniques for Tangible AI Outcomes
mfonobong
2
450
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
3.5k
Kristin Tynski - Automating Marketing Tasks With AI
techseoconnect
PRO
0
280
Marketing to machines
jonoalderson
1
5.5k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
HU Berlin: Industrial-Strength Natural Language Processing with spaCy and Prodigy
inesmontani
PRO
0
420
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
5.9k
jQuery: Nuts, Bolts and Bling
dougneiner
66
8.5k
The SEO Collaboration Effect
kristinabergwall1
1
490
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
170
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
370
Transcript
unittest.mockを使って テストを書こう ~モックオブジェクトを使ってより単体テストの目的に沿ったテストに~ 2020/02/29 PyCon mini Shizuoka @mizzsugar0425
お前、誰よ • みずきと申します。 • Twitter : @mizzsugar0425 • 仕事はデータ分析基盤の開発・運用です。 ◦
GCP, BigQuery • 趣味はPythonでWeb開発です。 ◦ Django, Pyramid, Nuxt.js, TypeScript, PostgreSQL • Djangogirlsコーチやってます。 • コーヒーと自転車が好きです。
このトークの対象者 • unittest.mockの存在は知っているけれどもイマイチ使いどころが分かって いない人 • assertTrueやassertEqualなどの基本的なテストコードの文法は知ってい るけれど、何のためにテストを書くかをじっくり考えたことがない人
前提 • unittest.mock特有の話ではないので、pytestでも使えます。 • 単体テストのことを話します。 • E2Eテストやシステムテストや結合テストについては話しません。 • テスト駆動開発については話しません。 •
スライド内に収めるためにサンプルコードがPEP8に反していることがありま すがご了承ください。 • 議論は大歓迎です! ただし、強い言葉や否定の言葉のない優しい世界 でお願いします(>人<)
本日の発表のソースコード https://github.com/mizzsugar/pycon_mini_shizuoka_2020
このトークは2部に分かれます 前半 単体テストの目的 後半 単体テストでモックオブジェクトの使い方
前半
今日これだけは持ち帰ってほしい(前半) • 単体テストの目的はクラスや関数の振る舞いが意図通りかの確認 • テストコードで単体テストを自動化 • 単体テストでは一つのテストで一つのことだけテスト
単体テストって? • 単体テストの目的 ◦ 作成したクラスや関数が意図した通りに振る舞うかの確認 • 単体テストのテストコードを書いたら嬉しいこと ◦ テストを実行するコマンド一つですべてのテストケースを実行してくれ ます
◦ 振る舞いを変更した場合に正しく振る舞えるかコマンド一つで確認でき ます
合計額を算出する関数をテストしよう calculate.py import dataclasses from typing import Iterable @dataclasses.dataclass(frozen=True) class
Item: name: str price: int def price(items: Iterable[Item]) -> int: return int(sum(item.price for item in items) * 1.08)
テストコードを書いてみよう① import unittest import src.calculate class TestCalculate(unittest.TestCase): def test_tax_should_be_included_in_sum(self): #
合計金額に8%の消費税も含まれて算出されるか確認 item_1 = src.calculate.Item(name='sample_1', price=500) item_2 = src.calculate.Item(name='sample_2', price=1000) items = [item_1, item_2] # 続きのコードは後ほど unittestモジュールでのテストお作法① unittest.TestCaseクラスを継承したクラスを作成。
テストコードを書いてみよう② class TestCalculate(unittest.TestCase): def test_tax_should_be_included_in_sum(self): # 合計金額に8%の消費税も含まれて算出されるか確認 item_1 = src.calculate.Item(name='sample_1',
price=500) item_2 = src.calculate.Item(name='sample_2', price=1000) items = [item_1, item_2] unittestモジュールでのテストお作法② テストクラスにテストメソッドを作成。 test_から始まらないと実行されません。
テストコードを書いてみよう③ unittestモジュールでのテストお作法③ アサーションメソッドで 期待通りの実行結果になるか検証 class TestCalculate(unittest.TestCase): def test_tax_should_be_included_in_sum(self): # 中略
actual = src.calculate.price(items) expected = 1620 self.assertEqual(expected, actual)
テストコードがあると楽に安心して変更できる ※今回の例はわかりやすさを重視して厳密な軽減税率のルールには則っていません。 @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) 軽減税率が導入されて イートインか判定する引数が 追加された
軽減税率導入に伴い、テストケース見直し def test_calculate_sum_eatin(self): # 中略 actual = src.calculate.price(items, eat_in=False) expected
= 1620 self.assertEqual(expected, actual) def test_calculate_takeout(self): # 中略 actual = src.calculate.price(items, eat_in=True) expected = 1650 self.assertEqual(expected, actual) 振る舞いの変更 に応じて テストメソッド名 も変更 イートインの場合と テイクアウトの場合の テストケースを作成 関数の呼び出し方も変更
キャッシュレスなら5%減額されるようになった calculate.py この後に更にルールの追加、変更・・・ となると 手動で関数の振る舞いが正しいかを確認するのが大変です。 def price( items: Iterable[Item], eat_in: bool,
cash_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 app.calculate >>> item1 = app.calculate.Item('item1', 500) >>>
item2 = app.calculate.Item('item2', 1000) >>> items = [item1, item2] >>> app.calculate.price(items, True, True) 1567 >>> 1500 * 1.1 * 0.95 1567.5000000000002 >>> # ↑ int()でくくるの忘れてた(本気でミスした) >>> int(1500 * 1.1 * 0.95) 1567 >>> app.calculate.price(items, False, True) 1539 >>> int(1500 * 0.8 * 0.95) 1140 >>> # ↑ 1.08を間違えて0.8にしてしまいました(真面目にタイプミスした) >>> int(1500 * 1.08 * 0.95) 1539
キャッシュレス導入に伴い、テストケース見直し class TestCalculate(unittest.TestCase): # テイクアウトかつ現金 def test_calculate_sum_takeout_cache(self): # 中略 actual
= src.calculate.price(items, eat_in=False, cash_less=False) expected = 1620 self.assertEqual(expected, actual) # イートインかつ現金 def test_calculate_takeout_cache(self): # 中略 actual = src.calculate.price(items, eat_in=True, cash_less=False) expected = 1650 self.assertEqual(expected, actual)
class TestCalculate(unittest.TestCase): # 中略 # テイクアウトかつキャッシュレス def test_calculate_sum_eatin_cashless(self): # 中略
actual = src.calculate.price(items, eat_in=False, cash_less=True) expected = 1539 self.assertEqual(expected, actual) # イートインかつキャッシュレス def test_calculate_takeout_cashless(self): # 中略 actual = src.calculate.price(items, eat_in=True, cash_less=True) expected = 1567 self.assertEqual(expected, actual)
テストコードがないと・・・ • 変更があるたびに手動で振る舞いを確認しないといけません • どこまで検証したのか第三者に分かりづらいです テストコードがあると・・・ • コマンド一つで振る舞いを確認できます • テストケースの内容をみるとどう検証したか確認できます
テストコードで単体テストを自動化しよう
一息つきます
モックの話に移ります
後半
今日これだけは持ち帰ってほしい(後半) • 「モック」とはオブジェクトや関数のふりをした偽物として振る舞うこと • モックを使うことで関係ないところでテストが左右されないように • Pythonでモックするならunittest.mock
外的要因にテストが左右されないことが大事 • 確認したいこと以外の箇所でテストの結果が左右されないべき • 関数内で呼び出している関数も実行させるとテストが失敗した時に要因を 探るのが大変なときも
例えば・・・
こんな関数をテストしたいとします kuji.py import random def kuji() -> str: is_lucky =
random.choice([True, False]) if is_lucky: return 'あたり' return 'はずれ'
テストが通る時と通らない時がある事件発生 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): actual = src.kuji.kuji() expected = 'はずれ' self.assertEqual(expected, actual) これじゃあ このテストが通れば安心と自 信をもちきれない!!
なぜ不安定なテストになってしまったのか class TestKuji(unittest.TestCase): def test_win_if_multiple_of_2(self): actual = src.kuji.kuji() expected =
'あたり' self.assertEqual(expected, actual) random.choiceが 実行されているため、 Falseが利用されることもあ る。
1つのことだけ確認したい • 標準モジュールで保証されているrandom.choice自体の動きまで確認したくありま せん • random.cohiceの出力によって「あたり」が出るか「はずれ」が出るかが大事
関係ないものは「モック」しよう
関係ないものは「モック」しよう • 「モック」という言葉には「偽の」「まがいの」という意味があります。 • 他のオブジェクトや関数のふりをした偽物として振る舞うイメージ • テストしたくない箇所はテスト内で実際に実行させず、実行させる「ふり」をさせま しょう。 • ふりをさせることで、関係ないものにテストが左右されず安定したテストに近づけま
す。
Pythonでモックするにはunittest.mock • Pythonでテストする際に利用するPythonの標準ライブラリ。 • プロダクトコードの一部をモックオブジェクトに置き換え、作成した関数やモ ジュールの振る舞いに関するアサーションメソッドを実行できる • モックオブジェクトに置き換えることで構築に手間がかかるオブジェクトを高 速に利用できる
モックオブジェクトでテストを改善 import random import unittest.mock class TestKuji(unittest.TestCase): def test_win_if_is_lucky(self): random.choice
= unittest.mock.Mock(return_value=True) actual = kuji.kuji() expected = 'あたり' self.assertEqual(expected, actual) return_valueでrandom.choice()が 返す値をTrueに固定します このテストメソッドでは常に Trueが返 され、 安定したテストになります。 テスト毎にランダムに値が出力される random.choiceをモックします
unittest.mock.patch デコレータを使ったモックの表現 @unittest.mock.patch('random.choice') def test_win_if_is_lucky(self, mock_choice): mock_choice.return_value=True actual = kuji.kuji()
expected = 'あたり' self.assertEqual(expected, actual) このテストメソッド内では、 random.choiceはmock_choiceという 変数で利用されます。
もっと役立つモックオブジェクト • テストが失敗する外的要因を排除 ◦ テストを実行する時間 ◦ タイムゾーン ◦ データベースの内容 ◦
ネットワークの状況 • とはいえ、モックオブジェクトに置き換えた箇所の動きは保証されていない のでよくテストされたものを使うなど注意が必要
unittest.mock相談室 1. unittest.mock.patchが当たらない 2. 関数が呼ばれたことだけを確認したい 3. datetime.datetime.nowをモックできない 4. 外部APIを利用したテストをどうかけばいいのかわからない
1. unittest.mock.patchがあたらない src/sample.py from random import randint def get_num(): return
randint(1, 10)
from … import … した関数が当たらない test_sample.py import src.sample class TestSample(unittest.TestCase):
def test_get_num_from_import_fail(self): random.randint = unittest.mock.Mock(return_value=1) actual = src.sample.get_num_from_import() expected = 1 self.assertEqual(actual, expected) randintをモックしたので 1を返すはずなのに 1にならない
秘密はfrom … import ...の仕組みにあった
from … import ...を書いたモジュールに変更 import src.sample class TestSample(unittest.TestCase): def test_get_num_from_import_fail(self):
src.sample.randint = unittest.mock.Mock(return_value=1) actual = src.sample.get_num() expected = 1 self.assertEqual(actual, expected)
2. 関数が呼ばれたことだけを確認したい sns.py import some_sns_module def send_share_sns(text: str, share: bool)
-> None: """textに指定した文字列をとあるSNSでシェアします""" if share: some_sns_module.share(text) # 実際にSNSに通信してほしくない
assert_called_once_withを使う assert_calledから始まる名前のテストメソッドを使うと、モックした関数が呼ば れたことを確認することが出来ます。 import sns class TestSNS(unittest.TestCase): @unittest.mock.patch('some_sns_module.share') def test_share(self,
mock_share): text = 'シェアしたい文字列' sns.send_share_sns(text, True) mock_share.assert_called_once_with(text)
3. datetime.datetime.now()をモックできない https://t-wada.hatenablog.jp/entry/design-for-testability をPythonに書き換え greet.py import datetime def greet() ->
str: now = datetime.datetime.now() if 5 <= now.hour < 12: return 'おはようございます' elif 12 <= now.hour < 18: return 'こんにちは' return 'こんばんは'
-> エラーになってテストできない test_greet.py class TestGreet(unittest.TestCase): def test_morning(self, mock_datetime): datetime.datetime.now =
unittest.mock.Mock( return_value=datetime.datetime( 2019, 10, 1, 5, 0, 0, 0 ) ) expected = 'おはようございます' self.assertEqual(expected, src.greet.greet())
案1: ライブラリ「freezegun」を使う test_greet.py from freezegun import freeze_time class TestGreet(unittest.TestCase): @freeze_time('2019-10-01
05:00:00') def test_morning(self): expected = 'おはようございます' self.assertEqual(expected, src.greet.greet())
案2: datetime.datetime.nowを引数にもつ テストはこんな感じになって・・・ test_greet.py class TestGreet(unittest.TestCase): def test_morning(self): expected =
'おはようございます' self.assertEqual( expected, src.greet.greet( lambda: datetime.datetime(2019, 10, 1, 5, 0, 0, 0) ) )
時間を引数にもたせると datetime.datetime.now()ではなく指定した日時で使いたい! という要望が出た時にも再利用できます。 def greet(time=datetime.datetime.now) -> str: now = time()
if 5 <= now.hour < 12: return 'おはようございます' elif 12 <= now.hour < 18: return 'こんにちは' return 'こんばんは'
外部APIを利用した関数のテストが書けない 日本の天気を取得するAPIを利用してピクニックの実施を判定する関数をテストするとし ます。 https://tenki.example.com/today/[郵便番号(7桁の半角数字ハイフンなし)] をGETでリクエストを送ると下記のようなJSONが返されます。 存在しない郵便番号を指定してリクエストが送られたらHTTPステータスコードが404でレ スポンスが返されます。 ※これは架空のAPIであり、実際には使用できません { '郵便番号':
'1050004' '都道府県': '東京都', '市区町村': '港区新橋', '天気': '晴れ' }
テスト対象の関数 import http import requests def go_picnic(postal_code: str) -> True:
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 True return False picnic.py
案① requests.getをモックする @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 = { '天気': '晴れ' } self.assertTrue(src.picnic.go_picnic1050004))
案②モックライブラリresponsesを使う import responses @responses.active def test_go_on_a_picnic_if_sunny(self): reponses.add( responses.GET, 'https://tenki.example.com/today/1050004', json={'天気':
'晴れ'}, status=200) self.assertTrue(src.picnic.go_picnic(1050004)) requestsを使っている場合のみ利用できます。
まとめ • 単体テストの目的はクラスや関数の振る舞いが意図通りかの確認 • テストコードで単体テストを自動化しましょう • 単体テストでは一つのテストで一つのことだけテスト • 「モック」とはオブジェクトや関数のふりをした偽物として振る舞うこと •
モックを使うことで関係ないところでテストが左右されないように • Pythonでモックするならunittest.mock
参考文献 • テスト駆動開発 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
Special Thanks • この発表の元ネタを話すきっかけを提供してくださった、Stapy2019年10月回の 運営の皆様 • CfP書くのを手伝ってくださった、nikkieさん、naruseさん • Pyhack冬合宿で発表練習に付き合ってくださった、 aodagさん、terapyonさん、 drillerさん、tananoryさん、Jonasさん、OEさん、kananさん、usaturnさん、
canzawaさん、hayaoさん、peacockさん、komo_frさん、tkoyamaさん
ありがとうございました!