Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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 で シードデータも挿入できる! 何も考えなくていいや😂

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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 の機能になる。

Slide 15

Slide 15 text

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): ...

Slide 16

Slide 16 text

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 でロールバックされ る。 テスト関数それぞれでトランザクション が張られる。

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Copyright © RevComm Inc. Thank you! 25

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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