Test Factories¶
Test factories generate mock database records. CKAN extensions can use factories in two ways:
- Pytest Fixtures: Powered by
pytest-factoryboy, these integrate with Pytest's dependency injection for declarative setup. - Direct Factory Classes: Imported directly from
ckan.tests.factoriesand called manually within test functions.
Pytest Fixture-Style Factories¶
Using pytest-factoryboy allows you to register factories as reusable
fixtures, making tests more "pytest'ish" and keeping test setups declarative.
Registering a Custom Extension Factory¶
To register a factory for a custom extension model, inherit from
ckan.tests.factories.CKANFactory and register it using the @register
decorator in tests/conftest.py.
The factory class must contain nested Meta class with reference to the
created model and name of API action responsible for creation.
import factory
from pytest_factoryboy import register
from ckan import model
from ckan.tests import factories
@register # (1)!
class FileFactory(factories.CKANFactory):
class Meta:
model = model.File # (2)!
action = "file_create" # (3)!
# 4. Define default attribute generators
name = factory.LazyFunction(factories.fake.unique.file_name)
upload = factory.Faker("binary", length=100)
- This decorator registers two fixtures: one is a function with camelized name
of the factory class (
file_factory) that produces objects; another uses camelized name of themodel(file) and contains the instance produced by this factory. - The SQLAlchemy model representing the custom database table.
- The logic action API responsible for inserting/validating the record.
- Generates unique file names and random binary payloads by default.
Defining Factory Attributes¶
To generate realistic mock data, you can configure factory fields using static values, sequential values, or random generators.
Static Attributes¶
For properties that should default to the same constant value across all entities:
class DocumentFactory(factories.CKANFactory):
# ...
format = "PDF" # Every document will have format="PDF" by default
Sequences¶
If a field must be unique (such as a username or code ID), use
factory.Sequence(). It passes a unique incrementing integer n (starting at
0) to your generator function:
class UserFactory(factories.CKANFactory):
# ...
# Generates: user-0, user-1, user-2, etc.
name = factory.Sequence(lambda n: f"user-{n}")
Lazy Functions¶
Use factory.LazyFunction() when you want to call a function (without
arguments) every time the factory generates an object. This is ideal for
timestamping or generating values dynamically:
from datetime import datetime
class ResourceFactory(factories.CKANFactory):
# ...
# Evaluated at the time of creation (not class load)
created = factory.LazyFunction(datetime.now)
Using Faker for Random Attributes¶
There are two ways to integrate Faker into your attribute declarations:
Using factory.Faker¶
This is a built-in provider that maps to Faker's generators. It is clean, concise, and supports passing parameters:
class ProfileFactory(factories.CKANFactory):
# ...
email = factory.Faker("email")
phone = factory.Faker("phone_number")
avatar = factory.Faker("image_url", width=120, height=120)
Using factory.LazyFunction with faker instances¶
If you need custom Faker configurations (like fetching unique values, or using
localized or nested fake data providers), instantiate a Faker object and pass
it inside a LazyFunction:
from faker import Faker
fake = Faker()
class DatasetFactory(factories.CKANFactory):
# ...
# Guarantees that the name will be unique across all test runs
name = factory.LazyFunction(fake.unique.company)
# Generates a localized address using the local Faker instance
location = factory.LazyFunction(lambda: fake.address())
Dependent Attributes (Lazy Attributes)¶
If an attribute's value depends on the value of another attribute of the same
object, use factory.LazyAttribute(). This accepts a lambda function receiving
the constructed entity instance (obj):
class UserFactory(factories.CKANFactory):
# ...
name = factory.Sequence(lambda n: f"user-{n}")
# Generates email based on the name attribute (e.g. user-0@example.com)
email = factory.LazyAttribute(lambda obj: f"{obj.name}@example.com")
Instance vs. Factory Fixture Variants¶
When you register a factory with @register, pytest-factoryboy creates two separate fixtures:
The Instance Fixture (entity)¶
Injecting the name of your entity (e.g. file or user) automatically
triggers the factory to write the object to the database and returns the
populated dictionary/model before the test body runs.
def test_file_details(file):
# 'file' is already created in the database
assert "id" in file
assert file["name"] is not None
This example is absolutely identical to the following:
def test_file_details(file_factory: types.TestFactory): # (1)!
file = file_factory()
assert "id" in file
assert file["name"] is not None
- Add
from ckan import typesto use typehints.
The Factory Fixture (entity_factory)¶
Injecting the name followed by _factory (e.g. file_factory or
user_factory) returns the callable factory class itself. This is useful when
you want to customize attributes for multiple instances or defer creation
inside the test.
def test_multiple_files(file_factory: types.TestFactory):
# Explicitly generate records with custom attributes
pdf = file_factory(name="document.pdf", upload=b"PDF_HEADER")
csv = file_factory(name="data.csv", upload=b"CSV_HEADER")
assert pdf["name"] == "document.pdf"
Direct Factory Classes¶
Alternatively, you can import and invoke factory classes directly inside test functions. This is useful for simple scripts but is less declarative than using Pytest's dependency injection.
Standard Core Factories¶
Core CKAN models have corresponding factory classes under ckan.tests.factories:
from ckan.tests import factories
import pytest
@pytest.mark.usefixtures("clean_db")
def test_dataset_association():
# 1. Create organization
org = factories.Organization(name="my-org")
# 2. Create user
user = factories.User(name="john")
# 3. Create dataset owned by organization
dataset = factories.Dataset(
name="my-dataset",
owner_org=org["id"]
)
# 4. Associate resources
resource = factories.Resource(
package_id=dataset["id"],
url="https://example.com/data.csv"
)
Comparison: Pytest Fixtures vs. Direct Classes¶
| Feature | Pytest-Factoryboy Fixtures | Direct Factory Classes |
|---|---|---|
| Call Style | Declarative parameters (user, user_factory) |
Procedural function calls (factories.User()) |
| Code Style | Clean and idiomatic Pytest, but extends test's signature | Can lead to boilerplate setup blocks in tests |
| Customization | Overrides passed as kwargs to _factory |
Overrides passed as kwargs to constructor |
Action Execution Context and User Parameters¶
Under the hood, CKANFactory calls CKAN core logic action APIs to persist entities in the database.
The user Parameter for Context¶
By default, every factory accepts a user parameter (which can be a user
dictionary returned by a factory, or a string representing a username). This
parameter is extracted from the arguments and placed inside the action
execution context dictionary (context = {"user": username}).
This ensures that the creation action runs on behalf of that specific user, which is critical if the action validates auth rules or sets owner fields:
def test_dataset_creator_context(package_factory, user):
# The package_create action is called with context['user'] = user['name']
dataset = package_factory(name="my-dataset", user=user)
assert dataset["creator_user_id"] == user["id"]
Exception: Named user Action Parameters¶
Some factories represent entities where user is a payload property of the
action itself rather than the actor context (for instance, the APIToken
factory requires a user parameter to designate which user is receiving the
token).
For these factories, passing the user argument acts as a parameter of the
action payload (as the API schema expects), rather than modifying the creator
context:
def test_api_token_assignment(api_token_factory, user):
# Here, 'user' is passed as a payload parameter to 'api_token_create'
# (indicating the recipient of the token), not the actor context.
token_dict = api_token_factory(user=user["id"])
assert token_dict["user_id"] == user["id"]
CKAN Core Factories Registry¶
CKAN core provides pre-registered pytest-factoryboy fixtures for all core
database entities. These are available in your test files automatically without
manual imports when pytest-ckan is active:
Factory Class (ckan.tests.factories) |
Instance Fixture | Factory Fixture | Model Target (ckan.model) |
Action Endpoint |
|---|---|---|---|---|
User |
user |
user_factory |
User |
user_create |
Sysadmin |
sysadmin |
sysadmin_factory |
User (sysadmin=True) |
user_create |
UserWithToken |
user_with_token |
user_with_token_factory |
User (with API Token) |
user_create + post token gen |
SysadminWithToken |
sysadmin_with_token |
sysadmin_with_token_factory |
User (with API Token) |
user_create + post token gen |
Dataset |
package |
package_factory |
Package |
package_create |
Resource |
resource |
resource_factory |
Resource |
resource_create |
ResourceView |
resource_view |
resource_view_factory |
ResourceView |
resource_view_create |
Group |
group |
group_factory |
Group |
group_create |
Organization |
organization |
organization_factory |
Group (is_organization=True) |
organization_create |
Vocabulary |
vocabulary |
vocabulary_factory |
Vocabulary |
vocabulary_create |
Tag |
tag |
tag_factory |
Tag |
tag_create |
APIToken |
api_token |
api_token_factory |
ApiToken |
api_token_create |
SystemInfo |
system_info |
system_info_factory |
SystemInfo |
Config store (custom method) |
File |
file |
file_factory |
File |
file_create |
Advanced Factory Methods¶
Factory classes and _factory fixtures support several advanced generation
strategies to return SQLAlchemy model instances, stub data without database
writes, or generate multiple entities at once.
The model Method¶
By default, calling a factory or its fixture writes the entity to the database via API and returns a plain python dictionary representing the serialized API response.
If your test needs to interact with the underlying SQLAlchemy database
model object instead of a dictionary (for instance, to assert relationship
mapping properties or run database session operations), call the .model()
class method:
from ckan import types
from ckan.tests import factories
from ckan.model import User
def test_user_model_instance(user_factory: types.TestFactory):
# Returns an instance of ckan.model.User instead of a dictionary
user_obj = user_factory.model(name="alice")
assert isinstance(user_obj, User)
assert user_obj.state == "active"
The stub Method¶
If your test only validates schemas, parameters, or forms, you might want to generate a set of valid dummy attributes without actually writing a record to the database or running time-consuming action workflows.
Call .stub() to return a local mock namespace object containing all the
randomly generated parameters the factory would have used:
def test_user_validation_form(user_factory):
# Generates attributes locally; no DB insert or API calls are run
stub_data = user_factory.stub(fullname="Alice Smith")
assert stub_data.fullname == "Alice Smith"
assert "@" in stub_data.email # Faker generated email
# Convert stub properties to a dictionary to simulate form POST body payload
form_payload = vars(stub_data)
The create_batch Method¶
To quickly seed a database with multiple mock records for listings, pagination,
or search index tests, use create_batch(size, **kwargs). This calls the
factory creation pipeline repeatedly to insert multiple records:
def test_dataset_pagination(package_factory):
# Create 15 datasets in the DB
datasets = package_factory.create_batch(15, owner_org="my-org-id")
assert len(datasets) == 15
assert datasets[0]["owner_org"] == "my-org-id"
Command-Line Data Generation¶
CKAN provides a command-line tool ckan generate fake-data to easily bootstrap
a development database with mock records.
Basic Generation¶
Generate a number of entities using built-in factories (aliases include user,
dataset, resource, organization, group):
# Generate 5 fake datasets in the database (outputs JSON for each)
ckan generate fake-data dataset -n 5
Generating Custom Extension Entities¶
If you have defined a custom factory inside your extension (e.g. FileFactory
registered inside ckanext.myextension.tests.conftest), you can invoke it by
passing its full Python import path as -f/--factory-class argument:
Passing Parameters to Factories¶
You can customize the generated data by passing factory options directly via
command-line arguments in the format --NAME=VALUE. These extra arguments are
collected and passed as keyword parameters to the factory's creation logic:
# Generate a dataset with a custom title and owner organization
ckan generate fake-data dataset --title="Special Core Data" \
--owner_org="my-org-id"
For custom factories, pass your specific parameters in the same manner: