Zenn
🐍

Djangoでテストデータを用意する

概要

半年前に転職して、現職でがっつり Python x Django を使用した開発を行っている。
これまで趣味で使うことや仕事で隙を見て使用することがあったが、これを期にしっかり学んでいきたい。

本記事では、Djangoをでユニットテストを行う上で、テストデータを用意する複数の方法についてそれぞれの特徴を簡単にではあるが整理していきたい。

テストデータを用意する3つの方法として以下を取り上げる。

  • モデル.objects.create を使用する
  • フィクスチャ を使用する
  • FactoryBoy を使用する

環境

  • Python : 3.13.0
  • Django : 5.1.6
  • FactoryBoy : 3.3.3

プロジェクト構成

  • sample という名前でプロジェクトを作る
  • sample ディレクトリ内でblogという名前でプロジェクトを作る
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 というモデルを定義しておく。
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

テストの実行方法

テストデータを用意する

モデル.objects.create でデータを投入する方法

特徴

  • 定義したモデルを用いて直接データを作成するシンプルな方法
  • Django ORM の機能をそのまま使える
  • データを毎回手動で用意する必要がある
  • 特定ケースのデータを用意したいときにコントロールしやすい

テストコード例

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 設定にリストされている任意のディレクトリ内
  • フィクスチャによって名前が付けられたリテラル パス内

本記事では、テスト用に使用することを念頭に使用するため、初期投入時のコマンドの説明はしない。

特徴

  • JSON/YAML/XML で事前にデータを保存しておくことができる
  • 毎回同じデータをロードできる
  • 一貫したデータでテスト可能

fixfureファイル

  • サンプルとなるfixfureファイルをblog/fixtures/blog.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"
    }
  }
]

テストコード例

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 に関連する記事がない")

いつ使うべきか

  • 一貫したデータを使いたい場合
  • 大量のデータを事前にロードして使う場合
  • DB の初期状態を固定したい場合

デメリット

  • データの編集が面倒
  • リレーションを手動で設定しないといけない
  • 変更が頻繁なテストには向かない

FactoryBoy を用いてテストデータを用意する

特徴

  • factory_boy をインストールする
  • 動的にデータを作成 できるので、毎回異なるテストデータを生成可能
  • リレーションのあるデータも簡単に作成
  • フィクスチャより柔軟性が高い

テストコード例

  • blog/models.py への記述例

factory_boy を用いたクラスて定義部分

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')

テストケース定義部分

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 に関連する記事がない")

いつ使うべきか

  • テストごとに異なるデータが必要な場合(ランダムなデータを用意したい)
  • リレーションが多い場合
  • 細かくカスタマイズしたい場合

デメリット

  • フィクスチャに比べると設定が必要
  • フィクスチャに比べると大量データを作成するには不向き

まとめ

使う場面の整理

方法 使う場面
objects.create() シンプルなテスト で一時的にデータを作成したい場合
フィクスチャ (fixtures) 事前に決まったデータを使い回したい場合
factory_boy 動的なデータを生成したい、リレーションが複雑なデータを扱う場合
  • factory_boy について、次回以降で学んでいきたい

参考URL

ENECHANGE

Discussion

ログインするとコメントできます