============================== Djangoでテストデータを用意する ============================== .. list-table:: :widths: 20 80 * - 公開日 - 2025-02-14 10:55 * - トピック - django, python, tdd * - Zenn - https://zenn.dev/fkzwsoftwaredev/articles/c562f8ebdff66c 概要 ---- 半年前に転職して、現職でがっつり Python x Django を使用した開発を行っている。 これまで趣味で使うことや仕事で隙を見て使用することがあったが、これを期にしっかり学んでいきたい。 本記事では、Djangoをでユニットテストを行う上で、テストデータを用意する複数の方法についてそれぞれの特徴を簡単にではあるが整理していきたい。 テストデータを用意する3つの方法として以下を取り上げる。 - モデル.objects.create を使用する - フィクスチャ を使用する - FactoryBoy を使用する 環境 ---- - Python : 3.13.0 - Django : 5.1.6 - FactoryBoy : 3.3.3 プロジェクト構成 ---------------- - sample という名前でプロジェクトを作る - sample ディレクトリ内でblogという名前でプロジェクトを作る .. code:: shell sample % tree . ├── blog │   ├── __init__.py │   ├── admin.py │   ├── apps.py │   ├── fixtures │   │   └── blog.json │   ├── migrations │   │   └── 0001_initial.py │   ├── models.py │   ├── tests.py │   └── views.py ├── db.sqlite3 ├── manage.py └── sample ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py 例として使用するモデル ---------------------- - blog/models.py の中身に Blog, Article というモデルを定義しておく。 .. code:: python from django.db import models class Blog (models.Model): title = models.CharField(max_length=100) description = models.TextField() author = models.CharField(max_length=100) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.title class Article (models.Model): blog = models.ForeignKey(Blog, on_delete=models.CASCADE) title = models.CharField(max_length=100) body = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.title テストの実行方法 ---------------- - `Djangoの基本的なテスト実行方法 `__ を用います。 - `pytest-django `__ を用いることもできる(勤め先のテストはこれ)が、こちらについて別の記事で学んでいきたい。 テストデータを用意する ---------------------- .. _モデルobjectscreate-でデータを投入する方法: モデル.objects.create でデータを投入する方法 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 特徴 ^^^^ - 定義したモデルを用いて直接データを作成するシンプルな方法 - Django ORM の機能をそのまま使える - データを毎回手動で用意する必要がある - 特定ケースのデータを用意したいときにコントロールしやすい テストコード例 ^^^^^^^^^^^^^^ .. code:: python from django.test import TestCase from .models import Blog, Article class BlogTestCase(TestCase): def setUp(self): blog = Blog.objects.create( title="Test Blog", description="This is a test blog", author="Test Author" ) articles = Article.objects.bulk_create([ Article( blog=blog, title=f"Test Article {i}", body="This is a test article" ) for i in range(100) ]) def test_blog_model(self): blog = Blog.objects.first() self.assertEqual(blog.title, "Test Blog", "Blog のタイトルが異なる") articles = blog.article_set.all() self.assertEqual(articles.count(), 100, "Blog に関連する記事がない") いつ使うべきか ^^^^^^^^^^^^^^ - 単純なデータを作る場合 - 少量のデータで十分な場合 - 細かいカスタマイズが必要な場合 デメリット ^^^^^^^^^^ - データの再利用が難しい(テストごとに書く必要がある)。 - リレーションが多いと手間がかかる - 複雑なテストデータを用意するのが面倒 フィクスチャ を用いて複数データを一括投入する方法 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ フィクスチャとは ^^^^^^^^^^^^^^^^ `fixfure `__\ は、テストやモデルに初期投入する際に用いるデータを保存しているファイルのことを言う。 :: フィクスチャ (fixture) とは、シリアライズされたデータベースのコンテンツを含むファイルのコレクションです。 各フィクスチャにはユニークな名前があり、フィクスチャを構成するファイルは、複数のアプリケーションの複数のディレクトリに分散できます。 以下のいずれかの場所に置いておく。 - インストールされた各アプリケーションの fixtures ディレクトリ内 - FIXTURE_DIRS 設定にリストされている任意のディレクトリ内 - フィクスチャによって名前が付けられたリテラル パス内 本記事では、テスト用に使用することを念頭に使用するため、初期投入時のコマンドの説明はしない。 .. _特徴-1: 特徴 ^^^^ - JSON/YAML/XML で事前にデータを保存しておくことができる - 毎回同じデータをロードできる - 一貫したデータでテスト可能 fixfureファイル ^^^^^^^^^^^^^^^ - サンプルとなるfixfureファイルをblog/fixtures/blog.json に保存する - データの内容を以下に示す .. code:: json [ { "model": "blog.Blog", "pk": 1, "fields": { "title": "Tech Blog", "description": "A blog about technology", "author": "Alice", "created_at": "2024-11-05T10:00:00Z", "updated_at": "2024-11-05T10:00:00Z" } }, { "model": "blog.Article", "pk": 1, "fields": { "blog": 1, "title": "Introduction to Django", "body": "Django is a high-level Python Web framework...", "created_at": "2024-11-05T10:10:00Z", "updated_at": "2024-11-05T10:10:00Z" } }, { "model": "blog.Article", "pk": 2, "fields": { "blog": 1, "title": "Advanced Django Models", "body": "Let's explore Django's ORM in detail...", "created_at": "2024-11-05T10:20:00Z", "updated_at": "2024-11-05T10:20:00Z" } } ] .. _テストコード例-1: テストコード例 ^^^^^^^^^^^^^^ .. code:: python from django.test import TestCase from .models import Blog, Article class BlogTestCase(TestCase): fixtures = ['blog.json'] def test_blog_model(self): blog = Blog.objects.first() self.assertEqual(blog.title, "Test Blog", "Blog のタイトルが異なる") articles = blog.article_set.all() self.assertEqual(articles.count(), 100, "Blog に関連する記事がない") .. _いつ使うべきか-1: いつ使うべきか ^^^^^^^^^^^^^^ - 一貫したデータを使いたい場合 - 大量のデータを事前にロードして使う場合 - DB の初期状態を固定したい場合 .. _デメリット-1: デメリット ^^^^^^^^^^ - データの編集が面倒 - リレーションを手動で設定しないといけない - 変更が頻繁なテストには向かない FactoryBoy を用いてテストデータを用意する ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. _特徴-2: 特徴 ^^^^ - `factory_boy `__ をインストールする - 動的にデータを作成 できるので、毎回異なるテストデータを生成可能 - リレーションのあるデータも簡単に作成 - フィクスチャより柔軟性が高い .. _テストコード例-2: テストコード例 ^^^^^^^^^^^^^^ - blog/models.py への記述例 **factory_boy を用いたクラスて定義部分** .. code:: python from factory import Faker, SubFactory, Sequence from factory.django import DjangoModelFactory from .models import Blog, Article class BlogFactory(DjangoModelFactory): class Meta: model = Blog title = Faker('sentence', nb_words=4) description = Faker('text') author = Faker('name') class ArticleFactory(DjangoModelFactory): class Meta: model = Article blog = SubFactory(BlogFactory) title = Faker('sentence', nb_words=6) body = Faker('text') **テストケース定義部分** .. code:: python class BlogFactoryBoyTestCase(TestCase): def setUp(self): self.actual_blog = BlogFactory() self.actual_articles = ArticleFactory.create_batch(100, blog=self.actual_blog) def test_blog_model(self): blog = Blog.objects.first() self.assertEqual(blog.title, self.actual_blog.title, "Blog のタイトルが異なる") for i, article in enumerate(self.actual_articles): print(article.title) articles = blog.article_set.all() self.assertEqual(articles.count(), len(self.actual_articles), "Blog に関連する記事がない") .. _いつ使うべきか-2: いつ使うべきか ^^^^^^^^^^^^^^ - テストごとに異なるデータが必要な場合(ランダムなデータを用意したい) - リレーションが多い場合 - 細かくカスタマイズしたい場合 .. _デメリット-2: デメリット ^^^^^^^^^^ - フィクスチャに比べると設定が必要 - フィクスチャに比べると大量データを作成するには不向き まとめ ------ 使う場面の整理 ~~~~~~~~~~~~~~ +-------------------------+----------------------------------------------------------------+ | 方法 | 使う場面 | +=========================+================================================================+ | objects.create() | シンプルなテスト で一時的にデータを作成したい場合 | +-------------------------+----------------------------------------------------------------+ | フィクスチャ (fixtures) | 事前に決まったデータを使い回したい場合 | +-------------------------+----------------------------------------------------------------+ | factory_boy | 動的なデータを生成したい、リレーションが複雑なデータを扱う場合 | +-------------------------+----------------------------------------------------------------+ - `factory_boy `__ について、次回以降で学んでいきたい 参考URL ------- - `Django テストを書いて実行する `__ - `Django フィクスチャ `__ - `FactoryBoy `__