Djangoでテストデータを用意する
公開日 |
2025-02-14 10:55 |
トピック |
django, python, tdd |
Zenn |
概要
半年前に転職して、現職でがっつり 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
テストの実行方法
Djangoの基本的なテスト実行方法 を用います。
pytest-django を用いることもできる(勤め先のテストはこれ)が、こちらについて別の記事で学んでいきたい。
テストデータを用意する
モデル.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 について、次回以降で学んでいきたい