Upgrade to Pro — share decks privately, control downloads, hide ads and more …

快適なテスト体験を実現する、Djangoのテスト思想と工夫

 快適なテスト体験を実現する、Djangoのテスト思想と工夫

RevComm_inc

July 05, 2023
Tweet

More Decks by RevComm_inc

Other Decks in Programming

Transcript

  1. Copyright © RevComm Inc.
    快適なテスト体験を実現する、
    Djangoのテスト思想と工夫
    2023.07.05
    Backend engineer 近藤智哉

    View full-size slide

  2. Copyright © RevComm Inc.
    contents
    1. 自己紹介
    2. 本日のテーマ
    3. Django testing に Deep-dive
    3.1. テスティングライブラリの概要
    3.2. テスト実行と DB の扱い
    3.3. Transaction を含むテスト
    3.4. いざ実行するときに困ること
    4. まとめ

    View full-size slide

  3. Copyright © RevComm Inc.
    1.自己紹介
    3
    󰢧 RevComm では MiiTel Meetings と 3rd party 連携を担当
    🗣 MiiTel Meetings でチームが国際化したことにより、英語
      モチベ高め
    💻 Frontend だと React、Backend では GraphQL にお熱、社外で
     は TS 使いがち
    ☕ コーヒーにハマっていたが、飲み過ぎでお腹痛くなったので最近
      は控えている
    近藤智哉
    こんどう もとや
    2021年8月入社
    Backend engineer

    View full-size slide

  4. Copyright © RevComm Inc.
    2.本日のテーマ
    4
    突然ですが、以下のような状況を想像してみてください。
    「API フレームワークに ORM を組み込んだ。コードでユニットテストしたい。」
    Nest.js (Flask/FastAPI) で作成し
    たバックエンドに、ORM を導入して
    DB 接続できるようにした!
    DB も含めたテスト書くぞ!
    昔の僕

    View full-size slide

  5. Copyright © RevComm Inc.
    2.本日のテーマ
    5
    たくさんの困難
    テストケースごとに DB を綺麗にして、シードデータを入れたい
    毎ファイルごとに Truncate と
    Migration、seed をしないといけない
    のか。。。
    Schema ごと吹っ飛ばせばいいの
    か?うーん。
    なんとかテスト用の DB を作れた。
    テストケースごとに DB を初期化し
    て、あらかじめテストのためのデー
    タを入れよう!

    View full-size slide

  6. Copyright © RevComm Inc.
    2.本日のテーマ
    6
    たくさんの困難
    ファイルの中の複数のテストを入れ替えると、テストが落ちたり落ちなかったり
    三井さんが、1 つ目と 2 つ目の間に
    テスト追加したら落ちるようになっ
    たって言ってたな。。。
    1つめの関数はユーザーの作成を
    テスト!
    2つ目はそのユーザー作成にフック
    される、メール送信処理をテスト!

    View full-size slide

  7. Copyright © RevComm Inc.
    2.本日のテーマ
    7
    たくさんの困難
    テストの実行に時間がかかる
    しかしテストすごい時間かかる。
    毎回時間がかかるから TDD やりた
    くなくなってきた。
    CI もすごい時間かかっていてお金
    も心配。。。
    TDD を心掛けていたから、テストも
    網羅的にかけていい感じ!

    View full-size slide

  8. Copyright © RevComm Inc.
    2.本日のテーマ
    8
    Django のテスティングライブラリなら解決するかも
    その悩み...。
    自分でテスト設計するの難しい😇
    毎ファイルごとに Truncate と
    Migration、seed をしないといけない
    のか。。。
    え。大変。。。
    三井さんが、1 つ目と 2 つ目の間に
    テスト追加したら落ちるようになっ
    たって言ってたな。。。
    しかしテストすごい時間かかる。
    毎回時間がかかるから TDD やりた
    くなくなってきた。
    CI もすごい時間かかっていてお金
    も心配。。。

    View full-size slide

  9. Copyright © RevComm Inc.
    2.本日のテーマ
    9
    from django.test import TestCase
    class TestUser(TestCase):
    @classmethod
    def setUpTestData(cls):
    cls.seeds = SeedFactory()
    def test_user_should_be_created(self):
    expected = {
    "name": "test",
    "email": "[email protected]",
    }
    user = User(**expected)
    user.save()
    self.assertTrue(user.name, expected["name"])
    self.assertTrue(user.email, expected["email"])
    test_user.py shell
    python manage.py test
    テストを django.test.TestCase を
    継承したクラスに記述。
    DB は自動でテスト用のものを作っ
    てくれて、setUpTestData で
    シードデータも挿入できる!
    何も考えなくていいや😂

    View full-size slide

  10. Copyright © RevComm Inc.
    2.本日のテーマ
    10
    󰢄 時間の都合上、今日話せないこと
    - Django の環境構築
    - Django での API 実装方法
    - Django でのテストの書き方
    - テストの方法論について
    - フロントエンド (HTTP) のテストについて
    Django のテスティングライブラリのDB周りの機能や設計を深掘りする。
    Django のテスティングライブラリの思想や工夫を読み取り、テスト機構を自作する際に活かせるようにす
    る。

    View full-size slide

  11. Copyright © RevComm Inc.
    2.本日のテーマ
    11
    参考資料や補足については以下のアイコンで記載する。
    ✍ 補足説明
    🔍 参考資料

    View full-size slide

  12. Copyright © RevComm Inc.
    from django.test import TestCase
    class TestUser(TestCase):
    @classmethod
    def setUpTestData(cls):
    cls.seeds = SeedFactory()
    def test_user_should_be_created(self):
      …
    test_user.py
    3.Django testing に Deep-dive
    12
    テスティングライブラリの概要 django.test
    - django の testing ライブラリはクラスベースである。
    TestCase
    - django.test.TestCase は unittest.TestCase をラップ
    している。django.test.TestCase を利用することで 
    Django のデータベースハンドリングも加味したテスト
    を作成できる。
    ✍ 補足説明
    クラスベース以外には、関数ベース (Jest) や Behavior-Driven Development
    (behave) などがある。
    ✍ 補足説明
    unittest.TestCase を利用すれば、テスト内で DB をハンドリングするコストを減ら
    すことができる。
    shell
    python manage.py test

    View full-size slide

  13. Copyright © RevComm Inc.
    from django.test import TestCase
    class TestUser(TestCase):
    @classmethod
    def setUpTestData(cls):
    cls.seeds = SeedFactory()
    def test_user_should_be_created(self):
      …
    test_user.py
    shell
    python manage.py test
    3.Django testing に Deep-dive
    13
    テスティングライブラリの概要 python manage.py test
    - test の対象となるファイルのパターンマッチングは、
    unittest の TestLoader.discover() によって実装され
    ている
    - django.test.TestCase のサブクラス
    → それ以外の Django-based なクラスのサブクラス
    → unittest.TestCase
    の順でテストが実行される
    ✍ 補足説明
    Test の実行の前に import が壊れていないかなどを確認してくれている。
    🔍 参考資料
    Test Discovery
    https://docs.python.org/3/library/unittest.html#unittest-test-discovery

    View full-size slide

  14. Copyright © RevComm Inc.
    3.Django testing に Deep-dive
    14
    テスト実行 1. Test 対象となる Class を取得
    2. データベースの準備
    a. データベースの新規作成
    b. データベースにマイグレーションを実行
    3. django.test.TestCase の実行
    a. setUpClass
    b. setUpTestData
    c. fixture 読み込み (_fixture_setup)
    d. 各テスト関数の実行
    i. setUp
    ii. テスト関数
    iii. tearDown
    e. fixture の破棄 (_fixture_teardown)
    f. tearDownClass
    4. データベースの削除
    class TestUser(TestCase):
    fixtures = ["user.json"]
    @classmethod
    def setUpTestData(cls):
    cls.seeds = SeedFactory()
    def setUp(self):
    # 各テストごとに作成したいデータ
    self.deleted_user = User(deleted_at=datetime.now())
    self.deleted_user.save()
    def tearDown(self) -> None:
    # 必要あればデータの削除等を行う

    def test_user_should_be_created(self):
    ...
    ✍ 補足説明
    リンクになっているものが django に
    より与えられている機能。リンクのな
    いものが unittest の機能になる。

    View full-size slide

  15. Copyright © RevComm Inc.
    1. Test 対象となる Class を取得
    2. データベースの準備
    a. データベースの新規作成
    b. データベースにマイグレーションを実行
    3. django.test.TestCase の実行
    a. setUpClass
    b. setUpTestData
    c. fixture 読み込み (_fixture_setup)
    d. 各テスト関数の実行
    i. setUp
    ii. テスト関数
    iii. tearDown
    e. fixture の破棄 (_fixture_teardown)
    f. tearDownClass
    4. テーブルの削除
    3.Django testing に Deep-dive
    15
    テスト実行
    関数ごとに Loop
    TestCase ごとに
    Loop
    class TestUser(TestCase):
    fixtures = ["user.json"]
    @classmethod
    def setUpTestData(cls):
    cls.seeds = SeedFactory()
    def setUp(self):
    # 各テストごとに作成したいデータ
    self.deleted_user = User(deleted_at=datetime.now())
    self.deleted_user.save()
    def tearDown(self) -> None:
    # 必要あればデータの削除等を行う

    def test_user_should_be_created(self):
    ...

    View full-size slide

  16. Copyright © RevComm Inc.
    関数ごとに Loop
    TestCase ごとに
    Loop
    1. Test 対象となる Class を取得
    2. データベースの準備
    a. データベースの新規作成
    b. データベースにマイグレーションを実行
    3. django.test.TestCase の実行
    a. setUpClass
    b. setUpTestData
    c. fixture 読み込み (_fixture_setup)
    d. 各テスト関数の実行
    i. setUp
    ii. テスト関数
    iii. tearDown
    e. fixture の破棄 (_fixture_teardown)
    f. tearDownClass
    4. テーブルの削除
    3.Django testing に Deep-dive
    16
    テスト実行と DB の扱い
    親トランザクション
    setUpClass 張られ、tearDownClass で
    ロールバックされる。
    TestCase ごとにトランザクションが張ら
    れ、全てのケースが実行されたらロール
    バックされる。
    子トランザクション
    _fixture_setup で張られ、
    _fixture_teardown でロールバックされ
    る。
    テスト関数それぞれでトランザクション
    が張られる。

    View full-size slide

  17. Copyright © RevComm Inc.
    3.Django testing に Deep-dive
    17
    Transaction を含むテスト
    🧐 Transaction はロールバックされてしまうので、Transaction が絡むコードはテストできな
    い?
    💡 できる!TransactionTestCase を利用する。

    View full-size slide

  18. Copyright © RevComm Inc.
    test_create_user.py
    from django.test import TransactionTestCase
    class TestUserEmail(TransactionTestCase):
    # send_email でメールが送信されたか確認する
    def test_failed_to_send_emai(self):
    create_user()
    self.assertEqual(True, User.object.first().email_sent)
    from django.db import transaction
    def create_user():
    with transaction.atomic():
    user = User.objects.create(name="test", email="[email protected]")
    user.save()
    transaction.on_commit(send_email, user)
    3.Django testing に Deep-dive
    18
    Transaction を含むテストを TransactionTestCase で実装する
    transaction.on_commit
    - トランザクションがコミットされるとコールバック関数
    が実行される。django.test.TestCase の場合は親のト
    ランザクションが張られ続けるのでコミットが発生せ
    ず、テストできない。
    🔍 参考資料
    Transaction on_commit 関数について
    https://docs.djangoproject.com/en/4.2/topics/db/transactions/#performing-act
    ions-after-commit
    create_user.py

    View full-size slide

  19. Copyright © RevComm Inc.
    TransactionTestCase
    - Transaction によって DB の状態を管理しない
    - DB への変更は全て commit される
    - 全てのテスト関数の実行後に、全ての Table を
    truncate する。
    test_create_user.py
    from django.test import TransactionTestCase
    class TestUserEmail(TransactionTestCase):
    # send_email でメールが送信されたか確認する
    def test_failed_to_send_emai(self):
    create_user()
    self.assertEqual(True, User.object.first().email_sent)
    from django.db import transaction
    def create_user():
    with transaction.atomic():
    user = User.objects.create(name="test", email="[email protected]")
    user.save()
    transaction.on_commit(send_email, user)
    3.Django testing に Deep-dive
    19
    Transaction を含むテストを TransactionTestCase で実装する
    ✍ 補足説明
    その他にもどうしても Test を跨いでデータを Commit させたい際などに利用でき
    る。
    create_user.py

    View full-size slide

  20. Copyright © RevComm Inc.
    3.Django testing に Deep-dive
    20
    いざ実行するときに困ること
    🧐 テストが毎回マイグレーションされるので、開発が滞る。
    💡 `--keepdb` オプションで、DB を破棄しないようにできる。
    🔍 参考資料
    --keepdb
    https://docs.djangoproject.com/en/4.2/ref/django-admin/#cmdoption-test-kee
    pdb

    View full-size slide

  21. Copyright © RevComm Inc.
    3.Django testing に Deep-dive
    21
    いざ実行するときに困ること
    🧐 テスト間に依存がないかチェックしたい
    💡 `--reverse` `--shuffle` オプションで、テストの実行順を変えることができる
    🔍 参考資料
    --shuffle
    https://docs.djangoproject.com/en/4.2/ref/django-admin/#cmdoption-test-shu
    ffle
    🔍 参考資料
    --reverse
    https://docs.djangoproject.com/en/4.2/ref/django-admin/#cmdoption-test-rev
    erse

    View full-size slide

  22. Copyright © RevComm Inc.
    3.Django testing に Deep-dive
    22
    いざ実行するときに困ること
    🧐 CI で動作をスピードアップさせたい。
    💡
    - テストクラス・関数にタグをつけることができ、タグごとに実行する
    - `--parallel` で並行実行する
    🔍 参考資料
    テストクラス・関数にタグをつける
    https://docs.djangoproject.com/en/4.2/topics/testing/tools/#tagging-tests
    🔍 参考資料
    --parallel
    https://docs.djangoproject.com/en/4.2/ref/django-admin/#cmdoption-test-par
    allel

    View full-size slide

  23. Copyright © RevComm Inc.
    4.まとめ
    23
    ● Transaction を利用し、テストごとに DB を綺麗にしている
    ● クラスベースであることを利用し、テストのシチュエーションごとに適切なサブクラスを提供
    している
    ● 実際に実行するときにカスタマイズしたい設定が、コマンド引数として提供されている
    わかったこと

    View full-size slide

  24. Copyright © RevComm Inc.
    4.まとめ
    24
    ● テスト(Class, 関数)は互いに独立しているべき
    ● テストはアプリケーションのためでもあり、エンジニアのためでもある。
    ○ 開発しやすさは大事
    ○ 「テスティングライブラリの制限によってテストできない」項目は存在してはいけない
    自分なりにまとめる Django テスティングライブラリの思想
    自分でテスト機構を設計していく際に
    取り入れていきたい!

    View full-size slide

  25. Copyright © RevComm Inc.
    Thank you!
    25

    View full-size slide

  26. Copyright © RevComm Inc.
    Appendix (おまけ)
    Django Testing Tips
    26

    View full-size slide

  27. Copyright © RevComm Inc.
    tearDown は実際どんなケースで利用するのか
    27
    django.test.TestCase を利用している限り、各関数内で新しく作られたレコードは、 Transaction
    のロールバックによって削除される。
    では tearDown はどのようなケースで有用なのか。
    - FileField のあるモデルを扱う場合、テスト実行の度にローカルにファイルが増えていくのを防ぐ
    ためにオブジェクトを削除する

    View full-size slide