Django "django.test" の "test.TransactionTestCase.assertNumQueries()" を徹底解説!


django.test.testcases.TransactionTestCase.assertNumQueries() は、Django テスト内で実行されたデータベースクエリ数を検証するためのメソッドです。このメソッドは、テスト対象のコードが想定よりも多くのクエリを実行していないことを確認するために使用されます。

使い方

assertNumQueries() メソッドは、以下の形式で使用します。

self.assertNumQueries(expected_num_queries, msg=None)
  • msg: 失敗時のエラーメッセージ (オプション)
  • expected_num_queries: 期待されるクエリ数

このメソッドは、テスト対象のコードが実行される前に呼び出されます。テスト対象のコードが実行されると、実際に実行されたクエリ数がカウントされます。その後、assertNumQueries() メソッドは、実際に実行されたクエリ数と expected_num_queries を比較します。一致しない場合は、テストが失敗し、msg (指定されていれば) がエラーメッセージとして表示されます。

以下の例は、BlogPost モデルの保存時に 1 つのクエリが実行されることを検証するテストコードです。

from django.test import TransactionTestCase
from .models import BlogPost

class BlogPostTest(TransactionTestCase):
    def test_save_blog_post(self):
        blog_post = BlogPost(title="My First Blog Post", body="This is my first blog post.")
        blog_post.save()

        self.assertNumQueries(1)
  • assertNumQueries() メソッドは、データベースクエリだけでなく、キャッシュ操作などもカウントします。
  • assertNumQueries() メソッドは、テスト対象のコード内でのみ使用できます。テスト対象外のコードで実行すると、予期しない結果になる可能性があります。
  • テスト対象のコードが想定よりも多くのクエリを実行している場合は、コードを最適化してクエリ数を減らす必要があります。
  • assertNumQueries() メソッドは、テスト対象のコードのパフォーマンスを向上させるために役立ちます。


例 1: 単一クエリのアサーション

from django.test import TransactionTestCase
from .models import BlogPost

class BlogPostTest(TransactionTestCase):
    def test_save_blog_post(self):
        blog_post = BlogPost(title="My First Blog Post", body="This is my first blog post.")
        blog_post.save()

        self.assertNumQueries(1)

例 2: 複数のクエリのアサーション

この例は、BlogPost モデルを作成してタグを追加する際に 2 つのクエリが実行されることを検証するコードです。

from django.test import TransactionTestCase
from .models import BlogPost, Tag

class BlogPostTest(TransactionTestCase):
    def test_create_blog_post_and_add_tag(self):
        tag = Tag.objects.create(name="My Tag")
        blog_post = BlogPost(title="My First Blog Post", body="This is my first blog post.")
        blog_post.tags.add(tag)
        blog_post.save()

        self.assertNumQueries(2)

例 3: エラーメッセージの指定

この例は、BlogPost モデルの更新時に 1 つのクエリが実行されることを検証し、もし予想よりも多くのクエリが実行された場合にエラーメッセージを表示するコードです。

from django.test import TransactionTestCase
from .models import BlogPost

class BlogPostTest(TransactionTestCase):
    def test_update_blog_post(self):
        blog_post = BlogPost.objects.create(title="My First Blog Post", body="This is my first blog post.")
        blog_post.title = "My Updated Blog Post"
        blog_post.save()

        self.assertNumQueries(1, msg="Too many queries were executed.")

例 4: カスタムアサーションの使用

この例は、カスタムアサーションを使用して、BlogPost モデルのリストを取得する際に実行されるクエリ数が 1 つ以下であることを検証するコードです。

from django.test import TransactionTestCase
from .models import BlogPost

class BlogPostTest(TransactionTestCase):
    def assert_num_queries_less_than_or_equal_to_one(self, expected_num_queries, msg=None):
        actual_num_queries = self.db.connection.queries
        if actual_num_queries > expected_num_queries:
            self.fail(f"Expected {expected_num_queries} queries, but got {actual_num_queries}. {msg}")

    def test_get_blog_post_list(self):
        BlogPost.objects.create(title="My First Blog Post", body="This is my first blog post.")
        BlogPost.objects.create(title="My Second Blog Post", body="This is my second blog post.")

        self.assert_num_queries_less_than_or_equal_to_one(1)


手動でクエリ数をカウントする

最も基本的な方法は、テスト対象のコード内で実行されるクエリを手動でカウントすることです。

def test_save_blog_post(self):
    num_queries_before = self.db.connection.queries
    blog_post = BlogPost(title="My First Blog Post", body="This is my first blog post.")
    blog_post.save()
    num_queries_after = self.db.connection.queries
    num_queries_executed = num_queries_after - num_queries_before

    self.assertEqual(num_queries_executed, 1)

利点

  • テスト対象のコードの内部構造を深く理解できる
  • コードがシンプルで分かりやすい

欠点

  • テスト対象のコードが変更されると、テストコードも更新する必要がある
  • すべてのクエリを手動でカウントする必要があるため、煩雑になる可能性がある

サードパーティ製ライブラリを使用する

mockcount_queries などのサードパーティ製ライブラリを使用して、テスト対象のコードが実行するデータベースクエリを自動的にカウントすることができます。

from mock import patch
from count_queries import count_queries

def test_save_blog_post(self):
    with patch("django.db.backends.signals.connection_created") as mock_connection_created:
        def count_db_queries(connection):
            self.assertEqual(count_queries(connection), 1)

        mock_connection_created.connect_receiver(count_db_queries)
        blog_post = BlogPost(title="My First Blog Post", body="This is my first blog post.")
        blog_post.save()

利点

  • テスト対象のコードの内部構造を理解する必要がない
  • 手動でクエリ数をカウントする必要がない

欠点

  • テスト対象のコードが変更されると、テストコードも更新する必要がある場合がある
  • サードパーティ製ライブラリをインストールして設定する必要がある

テスト対象のコードを最適化する

場合によっては、テスト対象のコードを最適化することで、クエリ数を減らすことができます。

  • 不要なクエリを削除する
  • キャッシュを活用する

利点

  • データベースへの負荷が軽減される
  • テスト対象のコードのパフォーマンスが向上する

欠点

  • テスト対象のコードを理解し、変更する必要がある

最適な方法の選択

どの代替方法が最適かは、テスト対象のコードや状況によって異なります。

  • テスト対象のコードのパフォーマンスを向上させることが重要であれば、コードを最適化する方法がおすすめです。
  • より多くの自動化が必要であれば、サードパーティ製ライブラリを使用する方法がおすすめです。
  • シンプルで分かりやすい方法が必要であれば、手動でクエリ数をカウントする方法がおすすめです。
  • テストコードは、テスト対象のコードが正しく動作することを検証するものであり、パフォーマンスを測定するものではないことを覚えておいてください。
  • テスト対象のコードが想定よりも多くのクエリを実行している場合は、コードを最適化してクエリ数を減らすことを検討してください。