Pro Yearly is on sale from $80 to $50! »

unittest.mockを使ってテストを書こう

C98d379da6e5517afff697a6c5615e68?s=47 mizzsugar
August 28, 2020

 unittest.mockを使ってテストを書こう

PyConJP2020の「unittest.mockを使ってテストを書こう」の発表資料です。

C98d379da6e5517afff697a6c5615e68?s=128

mizzsugar

August 28, 2020
Tweet

Transcript

  1. unittest.mockを使って
 テストを書こう
 ~モックオブジェクトを使ってより効率的で安定したテストに~ 2020/08/28 PyConJP2020 @mizzsugar0425 1

  2. お前、誰よ • みずきと申します。 • Twitter : @mizzsugar0425 • PythonでWebサービスの開発をしています。 •

    コーヒーと自転車が好きです。 • 実はPyConの登壇は2回目。1回目は去年の飛び込みLT 「PyConJP2018で勇気をもらってPythonエンジニアになった話」 https://gihyo.jp/news/report/01/pyconjp2019/0002?page=4 • 今年は飛び込みじゃない登壇が出来て嬉しいです。 2
  3. このトークの対象者 • unittest.mockの存在は知っているけれどもイマイチ使いどころが分からな い人 3

  4. 前提 • unittest特有の話ではないので、pytestなど他のテストライブラリでも使え ます。 • 単体テストのことを話します。 • E2Eテストやシステムテストや結合テストについては話しません。 • テスト駆動開発については話しません。

    • スライド内に収めるためにサンプルコードがPEP8に反していることがありま すがご了承ください。 4
  5. このトークは3部に分かれます 1部 単体テストの目的 2部 単体テストでのunittest.mockの使い方 3部 unitest.mock Tips&アンチパターン 5

  6. 1部 単体テストの目的 6

  7. 今日これだけは持ち帰ってほしい(1部) • 単体テストの目的はクラスや関数の振る舞いが意図通りかの確認 • テストコードで単体テストを自動化 • 単体テストでは一つのテストメソッドで一つのことだけテスト 7

  8. 単体テストって? • 単体テストの目的 ◦ 作成したクラスや関数が意図した通りに振る舞うかの確認 • 単体テストのテストコードを書いたら嬉しいこと ◦ テストを実行するコマンド一つですべてのテストケースを実行してくれ ます

    ◦ 振る舞いを変更した場合に正しく振る舞えるかコマンド一つで確認でき ます 8
  9. 合計額を算出する関数をテスト 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 sum(item.price for item in items) 9
  10. テストコード class TestCalculate(unittest.TestCase): def test_tax_should_be_included_in_sum(self): item_1 = calculate.Item(name='ビール', price=500) item_2

    = calculate.Item(name='ワイン', price=1100) items = [item_1, item_2] actual = calculate.price(items) expected = 1600 self.assertEqual(expected, actual) 10
  11. テストコードがあると楽に安心して変更できる @dataclasses.dataclass(frozen=True) class Item: name: str price: int def price(

    items: Iterable[Item], discount_percentage: Optional[int] ) -> int: _sum = sum(item.price for item in items) if discount_percentage: discounted = (1 - discount_percentage / 100) return int(_sum * discounted) return _sum 割引クーポンが導入されて 割引率を表す引数が 追加された 11
  12. 割引クーポン導入に伴い、テストケース見直し class TestCalculate(unittest.TestCase): def test_calculate(self): # 中略 actual = src.calculate.price(items,

    discount_percentage=None) expected = 1600 self.assertEqual(expected, actual) def test_calculate_with_discount(self): # 中略 actual = src.calculate.price(items, discount_percentage=10) expected = 1440 self.assertEqual(expected, actual) 振る舞いの変更 に応じて テストメソッド名 も変更 割引なしの場合と 割引ありの場合の テストケースを作成 関数の呼び出し方も変更 12
  13. 割引クーポンの有効期間が適用されるように @dataclasses.dataclass(frozen=True) class Coupon: from_: datetime.date to: datetime.date discount_percentage: int

    class Error(Exception): pass class InvalidCouponError(Error): pass 13
  14. 14 def price(items: Iterable[Item], coupon: Optional[Coupon], date: datetime.date) -> int:

    _sum = int(sum(item.price for item in items)) if coupon: if not coupon.from_ <= date <= coupon.to: raise InvalidCouponError('このクーポンは使えません') discounted = (1 - discount_percentage / 100) return int(_sum * discounted) return _sum
  15. 手動でのテストだと・・・(読まなくて良いです) >>> import calculate >>> item1 = calculate.Item('ビール', 500) >>>

    item2 = calculate.Item('ワイン', 1100) >>> items = [item1, item2] >>> coupon = calculate.Coupon(from_=datetime.date(2020, 8, 1), to=datetime.date(2020, 8, 31), discount_percentage=10) >>> calculate.price(items, coupon=coupon, date=datetime.date(2020, 8, 29)) 1440 >>> 1600 * 1 - 10 / 100 1599.9 >>> # ↑()でくくるの忘れてた (本気でミスした) >>> int(1600 * (1 - 10 / 100)) 1440 >>> ... 15
  16. クーポンの有効期間導入に伴いテストケース見直し class TestCalculate(unittest.TestCase): # クーポンあり def test_calculate_with_validate_coupon(self): # 中略 coupon

    = calculate.Coupon( from_=datetime.date(2020, 8, 1), to=datetime.date(2020, 8, 31), discount_percentage=10 ) actual = calculate.price( items, coupon, date=datetime.date(2020, 8, 28) expected = 1440 self.assertEqual(expected, actual) 16
  17. # 有効期限切れのクーポン def test_raise_if_invalid_coupon(self): # 中略 self.assertRaises(calculate.InvalidCouponError): calculate.price( items, coupon,

    date=datetime.date(2020, 9, 1)) # クーポンなし def test_calculate_without_coupon(self): # 中略 actual = src.calculate.price( items, coupon=None, date=datetime.date(2020, 8, 28)) expected = 1600 self.assertEqual(expected, actual) 17
  18. テストコードがないと・・・ • 変更があるたびに手動で振る舞いを確認しないといけません • 何を検証したのか第三者に分かりづらいです テストコードがあると・・・ • コマンド一つで振る舞いを確認できます • テストケースの内容をみるとどう検証したか

    確認できます テストコードで単体テストを自動化しよう 18
  19. モックの話に移ります 19

  20. 2部 単体テストでのunittest.mockの使い方 20

  21. 今日これだけは持ち帰ってほしい(2部) • 「モック」とはオブジェクトや関数のふりをした偽物として振る舞うこと • モックを使うことで関係ないところでテストが左右されないようになります。 • MockとMagicMockならMagicMockを使いましょう。 21

  22. 外的要因にテストが左右されないことが大事 • 確認したいこと以外の箇所でテストの結果が左右されないべき • テスト対象の関数内で呼び出している関数も実行させると テストが失敗した時に要因を探るのが大変なときも 22

  23. 例えば・・・ 23

  24. こんな関数をテストしたいとします kuji.py import random def kuji() -> str: is_lucky =

    random.choice([True, False]) if is_lucky: return 'あたり' return 'はずれ' 24
  25. テストが通る時と通らない時がある事件発生 import app.kuji class TestKuji(unittest.TestCase): def test_win_if_True(self): actual = src.kuji.kuji()

    expected = 'あたり' self.assertEqual(expected, actual) def test_lose_if_False(self): actual = src.kuji.kuji() expected = 'はずれ' self.assertEqual(expected, actual) これじゃあ このテストが通れば安心と自 信をもてない!! 25
  26. なぜ不安定なテストになってしまったのか class TestKuji(unittest.TestCase): def test_win_if_True(self): actual = src.kuji.kuji() expected =

    'あたり' self.assertEqual(expected, actual) random.choiceの 実行結果がFalseになる 場合もある 26
  27. 1つのことだけ確認したい • 標準モジュールで保証されているrandom.choice自体の動きまで確認したくありま せん • random.choiceの出力によって「あたり」が出るか「はずれ」が出るかが大事 27

  28. 関係ないものはモックしよう 28

  29. 関係ないものはモックしよう • 「モック」という言葉には「偽の」「まがいの」という意味があります。 • プログラム内のオブジェクトや関数のふりをした偽物として振る舞うイメージ • Pythonでは、標準ライブラリ「unittest.mock」を使って、プログラム内の関数やオブ ジェクト(ここではまとめてコンポーネントと呼びます)を 仮のコンポーネントに置き換えます。 •

    テスト技術者は仮のコンポーネントの挙動を指定することが出来ます。 • 仮のコンポーネントに置き換えることで構築に手間がかかるオブジェクトを 高速に利用できます。 29
  30. この発表における「モック」という言葉の扱い① • xUnitの文脈では、プログラムの中でオブジェクトや関数やモジュール(以下これら をまとめて「コンポーネント」と呼びます)の代品として動く仮のコンポーネントを「テ ストダブル」と呼びます。 • 「テストダブル」のうち、テスト対象物が利用するコンポーネントが テスト技術者が指定した通りに挙動するものを 「テストスタブ」と呼びます。 •

    「テストダブル」のうち、テスト対象物が利用するコンポーネントにどのようなアクセ スがあったか記録するものを「テストスパイ」と呼びます。 • xUnitにおける「モックオブジェクト」は、 コンポーネントへのアクセスを検証するのに利用しますが、 「テストスパイ」と異なり処理の途中で検証します。 30
  31. この発表における「モック」という言葉の扱い② 31

  32. この発表における「モック」という言葉の扱い③ • unittest.mockでは、テストスタブとテストスパイ両方出来ます。 • unitest.mock.Mockクラスのインスタンスが両方を担います。 • この発表では、xUnitの文脈で「テストダブル」と呼んでいるものをまとめて「モックオ ブジェクト」と言います。 • この発表では、オブジェクトや関数やモジュールを

    xUnitでいうテストダブルに置き換えることを「モックする」と言います。 32
  33. モックオブジェクトでテストを改善 import unittest.mock class TestKuji(unittest.TestCase): def test_win_if_is_lucky(self): import random random.choice

    = unittest.mock.Mock(return_value=True) actual = kuji.kuji() expected = 'あたり' self.assertEqual(expected, actual) return_valueでrandom.choice()が 返す値をTrueに固定します このテストメソッドでは常に Trueが返 され、 安定したテストになります。 テスト毎にランダムに値が出力される random.choiceをモックします 33
  34. インスタンスを置き換えることも可能 >>> class Person: ... def __init__(self, name): ... self.name

    = name ... ... def greet(self, word): ... return word >>> >>> p = Person(name='taro') >>> p.greet('hello') 'hello' >>> m = unittest.mock.Mock() >>> m.greet('hello') <Mock name='mock.greet()' id='139941090044080'> 34 p自体をモックオブ ジェクトにしたい
  35. side_effectで例外が発生するように指定 >>> m = unittest.mock.Mock() >>> m.error.side_effect = ValueError('これはエラーです') >>>

    m.error() ... ValueError: これはエラーです >>> p = Person(name='Taro') >>> p.greet = unittest.mock.Mock(side_effect=ValueError('これはエラーです ')) >>> p.greet('hi!') ... ValueError: これはエラーです 35
  36. 関数が呼ばれたことを記憶 >>> m.greet('hello') >>> m.greet.assert_called() # 呼ばれたので正常 >>> >>> m.greet.assert_not_called()

    # 呼ばれたので例外が発生 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.8/unittest/mock.py", line 874, in assert_not_called raise AssertionError(msg) AssertionError: Expected 'greet' to not have been called. Called 1 times. >>> m.greet.assert_called_with('hello') # 引数に何を与えられたのかを記憶 36
  37. 引数を確認するならcall_args 37 >>> m.greet('hello') >>> m.greet.call_args # 引数を確認 call('hello') >>>

    m.greet.call_args[0] # 位置引数をタプルで確認 ('hello',) >>> m.greet.call_args[0][0] # 1番目の位置引数を確認 hello
  38. キーワード引数を確認 38 >>> m.greet(text='hello') >>> m.greet.call_args # 引数を確認 call(text='hello') >>>

    m.greet.call_args[1] # キーワード引数を辞書で確認 {'text': 'hello'} >>> m.greet.call_args.kwargs # kwargsでもキーワード引数を確認可 {'text': 'hello'} >>> m.greet.call_args.kwargs['text'] >>> hello
  39. 位置引数とキーワード引数両方使っている場合 39 >>> m.greet('hello', date=datetime.date(2020, 8, 28)) >>> m.greet.call_args #

    引数を確認 call('hello', date=datetime.date(2020, 8, 28)) >>> m.greet.call_args[0] # 位置引数を確認 ('hello',) >>> m.greet.call_args[1] # キーワード引数を確認 {'text': 'hello'} >>> m.greet.call_args.kwargs # キーワード引数を確認 {'text': 'hello'}
  40. call_args_listで呼び出された順番を確認 40 >>> m.greet('good morning') >>> m.greet('hello') >>> m.greet('good night')

    >>> m.greet.call_args_list [call('good morning'), call('hello'), call('good night')] expected = [(('good morning',), ),(('hello',), ),(('good night',), )] >>> m.greet.call_args_list == expected True
  41. call_countで呼び出された回数を確認 41 >>> m.greet('good morning') >>> m.greet('hello') >>> m.greet('good night')

    >>> m.greet.call_count 3
  42. 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という 変数で利用されます。 'package.module.ClassName' の形式にします。 42
  43. モックオブジェクトを使って嬉しいこと • テストが失敗する外的要因を排除 ◦ テストを実行する時間 ◦ タイムゾーン ◦ データベースの内容 ◦

    ネットワークの状況 43
  44. 休憩 44

  45. 休憩中にやること • 水分とりましょう • 深呼吸して落ち着きましょう • 折返し地点まで来ました。残り時間を確認しましょ う • 気合入れて再スタートしましょう!

    45
  46. unittest.mock.Mockクラスの仕組み 46

  47. MockクラスのインスタンスはCallable① • Callableとは、関数呼び出し出来るオブジェクトのこと >>> def f() -> str: ... return

    'This is Callable.' ... >>> >>> f # これはCallable <function f at 0x7f9b2bdab670> >>> f() # これはstr 'this is Callable' 47
  48. MockクラスのインスタンスはCallable② 48 >>> import unittest.mock.Mock >>> m = unittest.mock.Mock(return_value=3) #

    関数呼び出しした時の返 り値を指定 >>> m <Mock id='140304410490000'> >>> m() 3 >>> thing = ProductionClass() >>> thing.method = unittest.mock.Mock(return_value=3) >>> thing.method # これはCallable <MagicMock id='140304410551584'> >>> thing.method() # これはint 3
  49. MockとMagicMockの違い① • MagicMockはMockの上位互換 • 基本的な使い方は同じ • MockはPythonのマジックメソッドが実装されていない • MagicMockはほぼ全てのマジックメソッドが実装されている •

    MagicMockを使った方が便利 49
  50. MockとMagicMockの違い② MagicMockでは 通常のメソッドと同じようにマジックメソッドをモックできます。 50 >>> m = unittest.mock.MagicMock() >>> m.__str__.return_value

    = 'aa' >>> str(m) aa
  51. MockとMagicMockの違い③ Mockでは出来ません。 51 >>> m = unittest.mock.Mock() >>> m.__str__.return_value =

    'aaa' Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'method-wrapper' object has no attribute 'return_value'
  52. MockとMagicMockの違い④ magic method を mock するには、対象の method に対して関数や mock のインスタン

    スをセットします。もし関数を使う場合、それは第一引数に self を取る 必要があります。  https://docs.python.org/ja/3/library/unittest.mock.html#mocking-magic-methods 52 >>> def __str__(self): ... return 'a' ... >>> m = unittest.mock.Mock() >>> m.__str__ = __str__ >>> str(m) 'a'
  53. MockとMagicMockの違い⑤ magic method は通常のメソッドとはルックアップ方法が異なるので 2, magic method のサポートは特別に実装されています。そのため、サポートされているのは特定の magic method

    のみです。 Magic method はインスタンスではなくクラスからルックアップされるはずです。Python のバージョンによってこのルールが適用されるかどうかに違いがあります。サポートされ ているプロトコルメソッドは、サポートされているすべての Python のバージョンで動作す るはずです。 ※Python公式ドキュメントから引用。 色と強調はドキュメントとは別に自分でつけました。 53 https://docs.python.org/ja/3/library/unittest.mock.html#magicmock-and-magic-method-support
  54. 3部 unittest.mock Tips&アンチパターン 54

  55. unittest.mockアンチパターン • モックオブジェクトに置き換えている所をテストしていない • ソースコードを変更する際にモックオブジェクトの使い方を見直さない 55

  56. 置き換えている箇所をテストしていない • よくテストしている箇所、もしくはテストが不要なほど細分化された箇所のみをモック オブジェクトに置き換えるべきです。 • 標準ライブラリをテストするようなものは逆にテスト不要です。 56 def dump_dict(source): json.dumps(source).encode()

    テスト不要な関数の例
  57. モックオブジェクトの使い方を見直さない • ソースコードを変更する際に、それに関わるテストで使っているモックオブジェクトの 使い方を見直さないのはNGです。 • レビューで注意してチェックしましょう。 • mypyやflake8を使った静的解析で変更の漏れを防ぎましょう。 57

  58. 単体テストは通るが静的解析は通らない例① def list_applicants(date): """指定した日に申し込んだユーザー一覧を取得します。 """ ... def download_applicants_of_date_csv(date): for user

    in list_applicants(date): ... 58 この時、dateはdatetime.date型を使う想定 この時、dateはdatetime.date型を使う想定 app.py
  59. 単体テストは通るが静的解析は通らない例② class TestDownloadApplicantsOfDateCsv: def test_download_applicants_of_date_csv(self): date = datetime.date(2020, 8, 28)

    app.list_applicants = unittest.mock.MagicMock() app.list_applicants.return_value = iter([app.User(name='py')]) actual = app.download_applicants_of_date_csv(date) ... 59
  60. 単体テストは通るが静的解析は通らない例③ def list_applicants(date_range): """指定した期間に申し込んだユーザー一覧を取得します。 """ ... def download_applicants_of_date_csv(date): for user

    in list_applicants(date): ... 60 期間なので Tuple[datetime.date, datetime.date] に変更 この時、dateはdatetime.date型のまま app.py
  61. 単体テストは通るが静的解析は通らない例④ class TestDownloadApplicantsOfDateCsv: def test_download_applicants_of_date_csv(self): date = datetime.date(2020, 8, 28)

    app.list_applicants = unittest.mock.MagicMock() app.list_applicants.return_value = iter([app.User(name='py')]) actual = app.download_applicants_of_date_csv(date) ... 61 このテストは通りましたが 実行時にエラーになりました。
  62. 型ヒントで静的解析してバグを防ごう① def list_applicants(date_range: Tuple[datetime.date, datetime.date]) -> Iterator[User]: """指定した期間に申し込んだユーザー一覧を取得します。 """ ...

    def download_applicants_of_date_csv(date: datetime.date) -> IO[str]: for user in list_applicants(date): ... 62 app.py こうやって 型ヒントを書いて…
  63. 型ヒントで静的解析してバグを防ごう② >>> pip install mypy >>> mypy app.py app.py:23: error:

    Argument 1 to "list_applicants" has incompatible type "date"; expected "Tuple[datetme.date, datetime.date]" 63 download_applicants_of_date_csv内で 使っているlist_applicantsが 意図しない型の引数を受け取っているので 注意してくれます。
  64. mypyから指摘を受けてプロダクトコード修正 64 def list_applicants(date_range: Tuple[datetime.date, datetime.date]) -> Iterator[User]: """指定した期間に申し込んだユーザー一覧を取得します。 """

    ... def download_applicants_of_date_csv(date: datetime.date) -> IO[str]: for user in list_applicants(tuple(date, date)): ... app.py
  65. mypyから指摘を受けてテストコード修正 class TestDownloadApplicantsOfDateCsv: def test_download_applicants_of_date_csv(self): date = datetime.date(2020, 8, 28)

    app.list_applicants = unittest.mock.MagicMock() app.list_applicants.return_value = iter([app.User(name='py')]) actual = app.download_applicants_of_date_csv(date) expected_date_range = tuple(date, date) app.list_applicants.assert_called_with(expected_date_range) 65
  66. unittest.mock Tips 1. unittest.mock.patchが当たらない 2. 関数を実際に実行してほしくない 3. datetime.datetime.nowをモックできない 4. 外部APIを利用したテストをどうかけばいいのかわからない

    66
  67. 1. unittest.mock.patchがあたらない src/sample.py def get_num(): from random import randint return

    randint(1, 10) 67
  68. 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にならない 68
  69. 秘密はfrom … import ...の仕組みにあった 69

  70. 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) 70
  71. 2. 関数を実際に実行してほしくない sns.py import some_sns_module def send_share_sns(text: str) -> None:

    """textに指定した文字列をとあるSNSでシェアします""" some_sns_module.share(text) # 実際にSNSに通信してほしくない 71
  72. 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) mock_share.assert_called_once_with(text) 72
  73. 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 'こんばんは' 73
  74. -> エラーになってテストできない test_greet.py class TestGreet(unittest.TestCase): def test_morning(self, mock_datetime): datetime.datetime.now =

    unittest.mock.Mock( return_value=datetime.datetime( 2020, 8, 28, 11, 50, 0, 0 ) ) expected = 'おはようございます' self.assertEqual(expected, src.greet.greet()) 74
  75. 案1: ライブラリ「freezegun」を使う test_greet.py from freezegun import freeze_time class TestGreet(unittest.TestCase): @freeze_time('2020-8-28

    11:50:00') def test_morning(self): expected = 'おはようございます' self.assertEqual(expected, src.greet.greet()) 75 https://github.com/spulec/freezegun
  76. 案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(2020, 8, 28, 11, 50, 0, 0) ) ) 76
  77. 時間を引数にもたせると 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 'こんばんは' 77
  78. 外部APIを利用した関数のテストが書けない 日本の天気を取得するAPIを利用してピクニックの実施を判定する関数をテストするとし ます。 https://tenki.example.com/today/[郵便番号(7桁の半角数字ハイフンなし)] にGETでリクエストを送ると下記のようなJSONが返されます。 存在しない郵便番号を指定してリクエストが送られたら HTTPステータスコードが404でレスポンスが返されます。 ※これは架空のAPIであり、実際には使用できません { '郵便番号':

    '1050004' '都道府県': '東京都', '市区町村': '港区新橋', '天気': '晴れ' } 78
  79. テスト対象の関数 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()['天気'] == '晴れ': return True return False picnic.py 79
  80. 案① 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=http.httpStatus.OK

    ) # 他の項目も入れるべきかは後続の処理によります。 mock_request.return_value.json.return_value = { '天気': '晴れ' } self.assertTrue(src.picnic.go_picnic(1050004)) 80
  81. 案②モックライブラリresponsesを使う import responses class TestPicnic: @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を使っている場合のみ利用できます。 81
  82. まとめ • 単体テストの目的はクラスや関数の振る舞いが意図通りかの確認 • 単体テストでは一つのテストで一つのことだけテスト • 「モック」とはオブジェクトや関数のふりをした偽物として振る舞うこと • モックすることで関係ないところでテストが左右されないように •

    unittest.mockではモックした関数の振る舞いを指定できます • unittest.mockではモックした関数がどのように呼ばれたのか確認できます 82
  83. 参考文献 • テスト駆動開発 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 83