Testdaten generieren
Es macht Sinn, schon in der Entwicklungsphase mit größeren Datenmengen zu arbeiten. Wir wollen mit 10 verschiedenen Usern arbeiten, die Events in Kategorien anlegen. Später Sollen diese Testuser für bestehende Events auch noch Reviews schreiben können.
Damit wir diese Testuser nicht alle händisch eintragen können, schreiben wir uns ein eigenes Management-Command, dass uns Testuser erstellt.
Eine User-App erstellen
Alles was mit Usern zu tun hat, soll in der App users liegen.
Also legen wir diese App an, in der wir zentral sämtliche benutzerbezogene Logik bündeln, wie zum Beispiel später ein eigenes User-Modell, Authentifizierungslogik, Formulare, Views sowie die zugehörigen Templates.
uv run manage.py startapp users
Unser Verzeichnis sieht jetzt so aus:
event_manager
├── manage.py
├── db.sqlite3
├── events
├── users # << neu
Damit die App auch in unserem Projekt bekannt ist, müssen wir sie in den INSTALLED_APPS der settings.py eintragen:
Damit die Templates für die Registrierung von Usern später auch gefunden werden, müssen wir sie ganz oben in der Liste der Apps eintragen, damit Django zuerst in diesem Verzeichnis nach Templates sucht. Dazu später mehr.
So sollte der Eintrag in den INSTALLED_APPS aussehen:
INSTALLED_APPS = [
"users", # <= diesen Eintrag hinzufügen
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"events",
]
Verzeichnis für ein Managment-Commando anlegen
Ein Management-Commando in Django wird so aufgerufen:
uv run manage.py create_user
Dieses Kommando müssen wir allerdings erstmal entwickeln.
Dazu erstellen wir uns im neuen Verzeichnis users folgende Ordner- und Dateistruktur.
Alle Kommandos müssen in einem Unterordner namens management/commands liegen, damit Django sie als solche auch automatisch erkennt.
Die Datei create_user.py ist erstmal völlig leer.
event_manager
├── manage.py
├── db.sqlite3
├── events
├── users
├── migrations/
├── management
│ └───commands
│ │ create_user.py # << neue Datei
ein erstes Commando
Jedes eigene Kommando erbt von BaseCommand. Die Methode handler wird
überschrieben und mit eigenem Code angereichert. Wir schreiben nun folgenden
Code in die Datei event_manager/users/management/commands/create_user.py.
from django.core.management.base import BaseCommand
class Command(BaseCommand):
def handle(self, *args, **options):
print("Hello World!")
Jetzt können wir unser Kommando mal testweise ausführen. Dazu
starten wir manage.py mit dem Namen der Datei, also create_user,
allerdings ohne Dateisuffix .py
uv run manage.py create_user
Das war doch einfach! Jetzt können wir loslegen, um Testuser im
System anzulegen. Dazu müssen wir erstmal factory-boy installieren.
Factory-Boy hilft uns dabei, möglichst realitätsnahe Zufallsdaten zu erstellen, die wir dann
als Testdaten nutzen können.
Management Commands für Skripte und Tools
Django’s Management Commands sind der ideale Ort, um kleine Helferprogramme
zu starten wie Imports, starten von asynchronen Jobmanagern und vielem
mehr. Da BaseCommand auch das äußerst beliebte argparse [1] aus der Python
Standard-Bibliothek implementiert, kann man die Commands auch mit
Argumenten aufrufen, wie wir unten gleich sehen werden.
Das Projekt kann dann nahezu vollständig über manage.py
gesteuert werden.
Die User-Fabrik
Factory-Boy
Wir werden Factory Boy verwenden, um Dummy-Daten für unsere App zu generieren.
Bei Factory Boy handelt es sich um eine Bibliothek, die für automatisierte Tests
entwickelt wurde, aber auch für diesen Anwendungsfall gut funktioniert.
Factory Boy kann leicht konfiguriert werden, um zufällige, aber realistische
Daten wie Namen, E-Mails und Absätze zu generieren, da intern die
Faker-Bibliothek verwendet wird.
Wir erstellen sogenannte Factories,
die wir als Django-Model-Klasse anlegen. So kann man sich zum Beispiel eine UserFactory erstellen, die Testdaten für einen User erstellt.
Diese UserFactory werden wir später auch zum Erstellen von Testusern bei den Integrations- und Unittests verwenden.
Eine Übersicht aller Standard-Faker findet sich hier: https://faker.readthedocs.io/en/master/providers.html
Faker bzw. dessen Provider bieten die Erstellung von allen möglichen
Testdaten. Sogar die Lokalisierung auf eine Sprache ist möglich, wenn man
zum Beispiel nur deutsche Usernamen generieren möchte. Wir nutzen den
default-Case, und der ist englisch.
Installieren wir nun factory-boy unseren Dev-Abhängigkeiten hinzu (produktiv wollen wir das Paket ja nicht nutzen, da es nur für die Entwicklung und Tests relevant ist).
uv add --dev factory-boy
Um Dummy-Daten zu erstellen, müssen wir eine sogenannte Fabrik
erstellen, die uns Dummy-Daten generiert.
Dazu legen wir die Datei event_manager/users/factories.py an:
event_manager
├── events
├── users
├── factories.py # << neue Datei
├── management
│ └───commands
│ │ create_user.py
und füllen Sie mit folgendem Inhalt:
from typing import Final
import factory
from django.contrib.auth import get_user_model
PASSWORD: Final = "abc"
user_list = ["Bob", "Alice", "Grumpy", "Waldo", "Charlie",
"Dave", "Eve", "Frank", "Grace", "Heidi",]
class UserFactory(factory.django.DjangoModelFactory):
"""
Factory zur Erzeugung von User-Instanzen für Tests.
Ziel:
- Schnelles Erstellen konsistenter Testdaten
- Vermeidung von Boilerplate beim Anlegen von Usern
- Korrekte Behandlung von Passwörtern (Hashing!)
Unterstützt sowohl:
- build(): erzeugt ein Objekt ohne DB-Zugriff
- create(): erzeugt und speichert das Objekt in der Datenbank
"""
class Meta:
model = get_user_model()
django_get_or_create = ("user_name",)
# Verhindert automatisches Speichern nach post_generation Hooks
# Wir wollen das selber machen, wegen Password-Hashing
skip_postgeneration_save = True
# Liefert nacheinander feste Usernamen aus der user_list
user_name = factory.Iterator(user_list)
# Generiert eindeutige E-Mail-Adressen
email = factory.Sequence(lambda n: f"user{n}@example.com")
@factory.post_generation
def password(self, create, extracted, **kwargs):
"""
Setzt das Passwort korrekt über set_password (inkl. Hashing).
Verhalten:
- Wird kein Passwort übergeben → Default-Passwort wird verwendet
- Wird ein Passwort übergeben → dieses wird gesetzt
Wichtig:
- set_password speichert NICHT automatisch
- Speicherung erfolgt nur bei create(), nicht bei build()
"""
pw = extracted or PASSWORD
self.set_password(pw)
if create:
self.save()
Ein User hat drei zentrale Attribute, die zwingend erforderlich sind, um ihn sinnvoll verwenden zu können: E-Mail-Adresse, Passwort und einen Usernamen.
Für reproduzierbare Tests vergeben wir keine komplett zufälligen Usernamen, sondern arbeiten mit einer festen, vordefinierten Menge von 10 Namen. Die E-Mail-Adressen hingegen werden automatisch eindeutig generiert.
Factory Boy bringt fertige Basisklassen für verschiedene ORMs mit, unter anderem auch für Django. Damit unsere Factory sauber mit dem Django-ORM zusammenarbeitet, erben wir von factory.django.DjangoModelFactory.
In der Meta-Klasse der UserFactory referenzieren wir das aktuell konfigurierte User-Modell über get_user_model(). Dadurch bleibt die Factory flexibel, selbst wenn später ein eigenes User-Modell eingeführt wird.
Die Meta-Klasse kennen wir bereits aus dem Event-Model und werden sie später auch bei der Formularverarbeitung wiedersehen.
Der @factory.post_generation-Hook wird verwendet, um das Passwort nachträglich korrekt über set_password zu setzen, da es nicht wie ein normales Feld behandelt werden kann.
Passwörter dürfen niemals im Klartext gespeichert werden, sondern müssen gehasht werden. Durch die Verwendung von set_password wird automatisch ein sicherer Hash erzeugt.
Standardmäßig vergeben wir für Testzwecke ein einfaches Passwort (z. B. abc), das bei Bedarf in Tests überschrieben werden kann.
Beispielhafte Verwendung der UserFactory:
# 1. Mit expliziten Werten
user = UserFactory(user_name="max", password="geheim123")
# → erstellt einen User mit gegebenem Usernamen und Passwort
# 2. Ohne Angaben (Default-Verhalten)
user = UserFactory()
# → nutzt vordefinierte Usernamen (Iterator)
# → generiert automatisch eine eindeutige E-Mail
# → setzt Standard-Passwort (z. B. "abc")
Mehr dazu in der Doku von Factory-Boy: https://factoryboy.readthedocs.io/en/stable/orms.html#django
Das Commando ausbauen
Wir ersetzen jetzt den Inhalt der Datei event_manager/users/management/commands/create_user.py mit folgenden Inhalt:
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from users.factories import UserFactory
User = get_user_model()
class Command(BaseCommand):
def add_arguments(self, parser) -> None:
"""Die Argumente für die Anzahl der User festlegen."""
parser.add_argument("-n",
"--number",
type=int,
help="Amount of users to be generated",
required=True)
parser.epilog = "Usage: manage.py create_user -n 10"
def handle(self, *args, **options):
# alle User bis auf den Adminuser löschen, damit wir immer mit einem sauberen
# System starten
User.objects.filter(is_superuser=False).delete()
# das geparste Kommandozeilenargument -n
number = options["number"]
if not 1 <= number <= 10:
raise SystemExit("Bitte eine Zahl zwischen 1 und 10 eingeben.")
for _ in range(number):
user = UserFactory()
print(f"=> {user}")
print(f"{number} User erfolgreich angelegt!")
Zuerst importieren wir die nötigen Module: unsere eben erstellte Fabrikklasse, das BaseCommand sowie die transaction und eine Helferfunktion zum Referenzieren auf das aktuelle UserModel.
Unsere Klasse Command startet mit der Methode add_arguments, die uns erlaubt, Argumente für einen Kommandozeilenparser zu spezifizieren. Django implementiert für diesen Zweck das brilliante argparse-Modul aus der Python Standard-Bibliothek.
Wir können das Management-Kommando später mit uv run manage.py create_user -n 10 starten und damit 10 User erstellen. Mehr als 10 User können wir nicht anlegen, da unser UserFactory nur 10 verschiedene Usernamen in der Liste hat. Durch das django_get_or_create = ("user_name",) ist sichergestellt, dass wir keine doppelten Usernamen anlegen können, da die Factory automatisch prüft, ob es bereits einen User mit diesem Namen gibt und diesen dann zurückgibt, anstatt einen neuen anzulegen. Zudem fragen wir im Management-Command die Anzahl der User ab, die angelegt werden sollen, und prüfen, ob sie im erlaubten Bereich liegt.
In der Methode handle löschen wir alle bestehenden User-Objekte bis auf das Objekt mit dem Superuser-Status, also dem Adminuser. Danach legen wir die mit der übergebenen Anzahl festgelegten User an.
Kommando auf der Konsole ausführen
Jetzt können wir unser Kommando nochmal ausführen.
uv run manage.py create_user -n 4
Das Programm startet jetzt und erstellt uns 4 Testuser:
$ uv run manage.py create_user -n 4
=> Bob
=> Alice
=> Grumpy
=> Waldo
4 User erfolgreich angelegt!
Wir sollten jetzt 4 Testuser in der Datenbank haben. Das prüfen wir entweder in der Admin oder auf der shell
>>> from django.contrib.auth import get_user_model
>>> get_user_model().objects.all()
<QuerySet [<User: admin>, <User: Bob>, <User: Alice>, ...]>
get_user_model liefert uns das aktuelle User-Model. Auf dem führen wir via dem Manager objects die Methode all aus.
Grundsätzlich sollte man immer get_user_model nutzen, wenn man Zugriff auf die User-Klasse benötigt, da diese Funktion immer das aktuell festgelegte User-Model nimmt.
Factories für Events und Categories
Nun wollen wir natürlich auch noch ein Kommando erstellen, um Kategorien und Events anzulegen.
Dazu legen wir auch im Verzeichnis events diese Dateistruktur an:
├───management
│ └───commands
│ │ create_events.py
und füllen event_manager/events/management/commands/create_events.py mit folgendem Inhalt:
"""
Erzeugen von Event-Daten
Dieses Modul stellt ein Management-Kommando bereit, um zufällige Event-Daten zu generieren.
Dabei werden ``factory_boy`` und die Bibliothek ``Faker`` verwendet.
"""
import random
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from events.factories import CategoryFactory, EventFactory
from events.models import Category, Event
class Command(BaseCommand):
def add_arguments(self, parser):
parser.description = "Generate Random Events and Categories"
parser.add_argument(
"-e",
"--events",
type=int,
help="Number of events to be generated",
required=True,
)
parser.add_argument(
"-c",
"--categories",
type=int,
help="Number of categories to be generated, max is 10",
required=True,
)
parser.epilog = "Usage example: python manage.py create_events -e 10 -c 3"
def handle(self, *args, **options):
num_events: int = options["events"]
num_categories: int = options["categories"]
if num_events < 0 or not 0 <= num_categories <= 10:
raise SystemExit(
"Nur nicht-negative Werte und maximal 10 Kategorien erlaubt."
)
print(f"Generating events={num_events}, categories={num_categories}")
User = get_user_model()
users = list(User.objects.all())
if not users:
raise SystemExit(
"Keine User vorhanden. Bitte zuerst: manage.py set_testusers"
)
print("Lösche vorhandene Daten...")
Event.objects.all().delete()
Category.objects.all().delete()
print("Erstelle Kategorien...")
categories = CategoryFactory.create_batch(num_categories)
print("Erstelle Events...")
for _ in range(num_events):
event = EventFactory(
category=random.choice(categories),
author=random.choice(users),
)
print(f"=> {event}")
Ok, hier ist eine Menge passiert. Sehen wir uns das mal an.
Zuerst einmal importieren wir alle nötigen Pakete und Module in unser Programm.
Dazu gehört auch das random-Modul, da wir Events zufällig Kategorien
zurordnen wollen. Die Fabriken fehlen allerdings noch.
import random
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from events.factories import CategoryFactory, EventFactory
from events.models import Category, Event
Das Kommando soll später mit Argumenten aufgerufen werden können, zum Beispiel
python manage.py create_events events=100 categories=4.
Die Klasse Command hat nun eine neue Methode: add_arguments. Dieser
Methodenname ist generisch und in dieser Methode geben wir Argumente an, die
an das Programm übergeben werden können.
Mit parser.description geben wir wie schon zuvor dem Argumentparser erstmal eine Beschreibung, die aufgerufen
wird, wenn zum Beispiel manage.py create_events --help ausgeführt
wird.
Dann erstellen wir zwei Arugmente, die als int geparste
und die via der help-Eigenschaft ebenfalls in der Hilfe angzeigt werden. Diese beiden Argumente bezeichnen die Anzahl der Events sowie der
Kategorien.
class Command(BaseCommand):
def add_arguments(self, parser):
parser.description = "Generate Random Events and Categories"
parser.add_argument(
"-e",
"--events",
type=int,
help="Number of events to be generated",
required=True,
)
parser.add_argument(
"-c",
"--categories",
type=int,
help="Number of categories to be generated, max is 10",
required=True,
)
parser.epilog = "Usage example: manage.py create_events -e 10 -c 3"
Wenn wir help für dieses Subkommando ausführen …
uv run manage.py create_events --help
erhalten wir diese Ausgabe (gekürzt), wie das Subkommando zu nutzen ist:
usage: manage.py create_events [-h] [-e EVENTS] [-c CATEGORIES] [--version]
[--no-color] [--force-color] [--skip-checks]
Generate Random Events and Categories
options:
-h, --help show this help message and exit
-e EVENTS, --events EVENTS
Number of events to be generated
-c CATEGORIES, --categories CATEGORIES
Number of categories to be generated, max is 10
Usage example: manage.py create_events events=100 categories=10
In der handle-Methode fragen wir erstmal die Argumente ab und prüfen, ob
sie in dem erlaubten Bereich liegen. Das heisst, die Zahl darf nicht negativ
sein und die Anzahl an Kategorien darf 10 nicht übersteigen. Die Kategorien
wollen wir später nämlich als Wortliste anlegen und nicht völlig zufällig
erzeugen.
num_events: int = options["events"]
num_categories: int = options["categories"]
if num_events < 0 or not 0 <= num_categories <= 10:
raise SystemExit(
"Nur nicht-negative Werte und maximal 10 Kategorien erlaubt."
)
print(f"Generating events={num_events}, categories={num_categories}")
Im nächsten Schritt selektieren wir die aktuellen User, die wir ja vorher schon
erstellt hatten. Befinden sich aktuell keine User im System, beendet das
Programm mit einem SystemExit. Die users benötigen wir für das
Erstellen eines Events, um aus ihr einen zufälligen User zu ziehen.
users = get_user_model().objects.all()
if not users:
print("Es existieren keine User im System.")
print("Bitte führe erst manage.py create_user aus")
raise SystemExit(1)
Nun löschen wir alle bisherigen Events und Kategorien, damit wir mit einem sauberen System starten können.
print("Lösche vorhandene Daten...")
Event.objects.all().delete()
Category.objects.all().delete()
Übrigens hätten wir die Events an dieser Stelle gar nicht löschen müssen, da die Events via einem ForeignKey mit den Kategorien in Beziehung stehen. Und da wir beim Erstellen dieser Verbindung models.CASCADE gewählt hatten, also das kaskadierende Löschen, würden alle Events ebenfalls beim Löschen der
Kategorien mitgelöscht. Aber sicher ist sicher, man weiß ja nie, was später noch passiert.
Danach erstellen wir Testdaten mit der (noch nicht existenten Category- sowie
Event-Factory). Die erstellten Kategorien sammeln wir dann in der Liste
categories, aus der wir später beim Erstellen eines Events zufällig einen
Eintrag ziehen. Dazu hatten wir die Methode create_batch genutzt und ihr
als Argument die Anzahl der Kategorien eingegeben, die der User erstellen will.
Beim Erstellen der Events ziehen wir zufällig eine Kategorie und einen User und
übergeben diese Werte dem EventFactory - Konstruktor.
print("Erstelle Kategorien...")
categories = CategoryFactory.create_batch(num_categories)
print("Erstelle Events...")
for _ in range(num_events):
event = EventFactory(
category=random.choice(categories),
author=random.choice(user_list),
)
Nun fehlen uns natürlich noch die Factories. Dazu legen wir die Datei
event_manager/events/factories.py
event_manager
├── events
├── factories.py # << neue Datei
├── management
│ └───commands
│ │ create_events.py
an und legen erstmal eine CategoryFactory an:
from datetime import timedelta
from django.contrib.auth import get_user_model
from django.utils import timezone
import factory
from users.factories import UserFactory
from . import models
User = get_user_model()
categories = [
"Sports",
"Talk",
"Cooking",
"Freetime",
"Hiking",
"Movies",
"Travelling",
"Science",
"Arts",
"Pets",
"Music",
"Wellness",
"Religion",
]
class CategoryFactory(factory.django.DjangoModelFactory):
"""Erstellt eine Kategorie aus einer vorgegebenen Liste."""
class Meta:
model = models.Category
django_get_or_create = ("name",)
name = factory.Iterator(categories)
sub_title = factory.Faker("sentence")
description = factory.Faker("paragraph", nb_sentences=20)
Die CategoryFactory erzeugt Kategorien auf Basis einer vordefinierten Liste von Namen und ergänzt diese automatisch um generierte Untertitel und Beschreibungen mittels Faker. Durch django_get_or_create wird sichergestellt, dass keine doppelten Kategorien mit identischem Namen erstellt werden.
Erstellen wir nun die EventFactory in der selben Datei event_manager/events/factories.py:
class EventFactory(factory.django.DjangoModelFactory):
"""Event Fabrik zum Erstellen eines neuen Events."""
class Meta:
model = models.Event
# Author und Category werden nur erzeugt, wenn sie beim Erstellen der
# Factory nicht überschrieben werden.
author = factory.SubFactory(UserFactory)
category = factory.SubFactory(CategoryFactory)
name = factory.Faker("sentence")
sub_title = factory.Faker("sentence")
description = factory.Faker("paragraph", nb_sentences=20)
min_group = factory.Faker("random_element", elements=models.Event.Group)
date = factory.Faker(
"date_time_between",
end_date=timezone.now() + timedelta(days=60),
start_date=timezone.now() + timedelta(days=1),
tzinfo=timezone.get_current_timezone(),
)
Diese generiert Events mit zufälligen Titeln, Beschreibungen, Daten, Teilnehmerzahlen sowie Verknüpfungen zu bestehenden Kategorien und Usern. Das Attribut min_group wird zufällig aus den erlaubten Werten des Event-Modells gezogen, während das Datum innerhalb eines realistischen Zeitraums liegt (zwischen heute und 60 Tagen in der Zukunft).
Beziehungen wie author und category werden automatisch über SubFactory erstellt, sofern sie nicht explizit beim Aufruf gesetzt werden. Deshalb hatten wie die UserFactory importiert.
Jetzt können wir Testdaten generieren und prüfen, ob sie in der Datenbank vorhanden sind.
uv run manage.py create_events --events 20 --categories 5
Wenn wir unsere Datenbank öffnen und die Tabelle events_events ansteuern,
sehen wir eine ganze Menge Einträge:
Zugegeben, die Namen und Beschreibungen machen keinen Sinn, aber immerhin haben wir valide Testdaten, um unser System besser zu testen.
Optional: Das Projekt in der Version v0.1 kopieren
Wer bisher mitgecoded hat, sollte das Projekt eigentlich zum Laufen gebracht haben. Falls es doch irgendwo einen Fehler gibt, den man nicht behoben bekommt, oder einfach nur so, kann sich das Projekt im aktuellen Zustand auch klonen oder runterladen.
Version v0.1 via github clonen
git clone -b v0.1 git@github.com:realcaptainsolaris/event_project.git
und dann nicht vergessen, ein virtuelles Environment anzulegen und zu migrieren:
uv sync
uv run manage.py migrate
uv run manage.py createsuperuser
uv run manage.py create_user -n 10
uv run manage.py create_events --events 20 --categories 5
uv run manage.py runserver
Version v0.1 als zip-Datei runterladen
https://github.com/realcaptainsolaris/event_project/archive/refs/tags/v0.1.zip
Version v0.1 als tar-Datei runterladen
https://github.com/realcaptainsolaris/event_project/archive/refs/tags/v0.1.tar.gz