Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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

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

RevComm_inc

July 05, 2023
Tweet

More Decks by RevComm_inc

Other Decks in Programming

Transcript

  1. Copyright © RevComm Inc. contents 1. 自己紹介 2. 本日のテーマ 3.

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

    Meetings と 3rd party 連携を担当 🗣 MiiTel Meetings でチームが国際化したことにより、英語   モチベ高め 💻 Frontend だと React、Backend では GraphQL にお熱、社外で  は TS 使いがち ☕ コーヒーにハマっていたが、飲み過ぎでお腹痛くなったので最近   は控えている 近藤智哉 こんどう もとや 2021年8月入社 Backend engineer
  3. Copyright © RevComm Inc. 2.本日のテーマ 4 突然ですが、以下のような状況を想像してみてください。 「API フレームワークに ORM

    を組み込んだ。コードでユニットテストしたい。」 Nest.js (Flask/FastAPI) で作成し たバックエンドに、ORM を導入して DB 接続できるようにした! DB も含めたテスト書くぞ! 昔の僕
  4. Copyright © RevComm Inc. 2.本日のテーマ 5 たくさんの困難 テストケースごとに DB を綺麗にして、シードデータを入れたい

    毎ファイルごとに Truncate と Migration、seed をしないといけない のか。。。 Schema ごと吹っ飛ばせばいいの か?うーん。 なんとかテスト用の DB を作れた。 テストケースごとに DB を初期化し て、あらかじめテストのためのデー タを入れよう!
  5. Copyright © RevComm Inc. 2.本日のテーマ 6 たくさんの困難 ファイルの中の複数のテストを入れ替えると、テストが落ちたり落ちなかったり 三井さんが、1 つ目と

    2 つ目の間に テスト追加したら落ちるようになっ たって言ってたな。。。 1つめの関数はユーザーの作成を テスト! 2つ目はそのユーザー作成にフック される、メール送信処理をテスト!
  6. Copyright © RevComm Inc. 2.本日のテーマ 7 たくさんの困難 テストの実行に時間がかかる しかしテストすごい時間かかる。 毎回時間がかかるから

    TDD やりた くなくなってきた。 CI もすごい時間かかっていてお金 も心配。。。 TDD を心掛けていたから、テストも 網羅的にかけていい感じ!
  7. Copyright © RevComm Inc. 2.本日のテーマ 8 Django のテスティングライブラリなら解決するかも その悩み...。 自分でテスト設計するの難しい😇

    毎ファイルごとに Truncate と Migration、seed をしないといけない のか。。。 え。大変。。。 三井さんが、1 つ目と 2 つ目の間に テスト追加したら落ちるようになっ たって言ってたな。。。 しかしテストすごい時間かかる。 毎回時間がかかるから TDD やりた くなくなってきた。 CI もすごい時間かかっていてお金 も心配。。。
  8. 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 で シードデータも挿入できる! 何も考えなくていいや😂
  9. Copyright © RevComm Inc. 2.本日のテーマ 10 󰢄 時間の都合上、今日話せないこと - Django

    の環境構築 - Django での API 実装方法 - Django でのテストの書き方 - テストの方法論について - フロントエンド (HTTP) のテストについて Django のテスティングライブラリのDB周りの機能や設計を深掘りする。 Django のテスティングライブラリの思想や工夫を読み取り、テスト機構を自作する際に活かせるようにす る。
  10. 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
  11. 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
  12. 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 の機能になる。
  13. 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): ...
  14. 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 でロールバックされ る。 テスト関数それぞれでトランザクション が張られる。
  15. Copyright © RevComm Inc. 3.Django testing に Deep-dive 17 Transaction

    を含むテスト 🧐 Transaction はロールバックされてしまうので、Transaction が絡むコードはテストできな い? 💡 できる!TransactionTestCase を利用する。
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. Copyright © RevComm Inc. 4.まとめ 23 • Transaction を利用し、テストごとに DB

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

    テストはアプリケーションのためでもあり、エンジニアのためでもある。 ◦ 開発しやすさは大事 ◦ 「テスティングライブラリの制限によってテストできない」項目は存在してはいけない 自分なりにまとめる Django テスティングライブラリの思想 自分でテスト機構を設計していく際に 取り入れていきたい!
  23. Copyright © RevComm Inc. tearDown は実際どんなケースで利用するのか 27 django.test.TestCase を利用している限り、各関数内で新しく作られたレコードは、 Transaction

    のロールバックによって削除される。 では tearDown はどのようなケースで有用なのか。 - FileField のあるモデルを扱う場合、テスト実行の度にローカルにファイルが増えていくのを防ぐ ためにオブジェクトを削除する