-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Generating Test Records in Frappe
This article provides an in-depth explanation of how test records are generated in Frappe using the make_test_records
entry point. This process is crucial for testing and development, allowing developers to create sample data for their applications.
The make_test_records
function is responsible for generating test records for a specified DocType and its dependencies. This function ensures that all necessary records are created to facilitate comprehensive testing of the application's functionality. However, it's important to note that users typically do not need to call this function directly, as test records are automatically loaded by an IntegrationTestCase
.
The make_test_records
function serves as the entry point for generating test records. It takes the following parameters:
-
doctype
: The name of the DocType for which test records need to be generated. -
force
: A boolean indicating whether existing records should be reset and regenerated. -
commit
: A boolean indicating whether changes should be committed to the database.
def make_test_records(doctype, force=False, commit=False):
"""Generate test records for the given doctype and its dependencies."""
return list(_generate_all_records_towards(doctype, reset=force, commit=commit))
Note: This function is typically invoked indirectly through an IntegrationTestCase
, which uses the current document as the entry point for loading test records.
This function generates test records for the specified DocType and its dependencies. It performs a depth-first traversal to ensure all dependencies are resolved before creating records.
def _generate_all_records_towards(index_doctype, reset=False, commit=False) -> Generator[tuple[str, int], None, None]:
"""Generate test records for the given doctype and its dependencies."""
visited = set(frappe.local.test_objects.keys())
for _index_doctype in get_missing_records_doctypes(index_doctype, visited):
res = list(_generate_records_for(_index_doctype, reset=reset, commit=commit, initial_doctype=index_doctype))
yield (_index_doctype, len(res))
This function creates and yields test records for a specific DocType. It handles custom logic defined in modules and loads test records from JSON or TOML files if available.
def _generate_records_for(index_doctype: str, reset: bool = False, commit: bool = False, initial_doctype: str | None = None) -> Generator[tuple[str, "Document"], None, None]:
"""Create and yield test records for a specific doctype."""
module, test_module = get_modules(index_doctype)
if hasattr(test_module, "_make_test_records"):
yield from test_module._make_test_records()
else:
if hasattr(test_module, "test_records"):
test_records = test_module.test_records
else:
test_records = load_test_records_for(index_doctype)
if isinstance(test_records, list):
test_records = _transform_legacy_json_records(test_records, index_doctype)
yield from _sync_records(index_doctype, test_records, reset=reset, commit=commit)
Note: Upstream developers may use the module level's test_records
attribute, when downstream applications disagree over these test records, so that they can be monkey patched:
# Upstream test_my_doctype.py
test_records = frappe.tests.utils.load_test_records_for("My Doctype")
# Donwnstream app's before_tests hook
import upstream.some.doctype.my_doctype.test_my_doctype
upstream.some.doctype.my_doctype.test_my_doctype.test_records = [] # overrides
This function synchronizes the provided test records with the database. It handles caching and persistence to ensure efficient record management. When regenerating records with force=True
, it may not necessarily reset existing records but instead create additional ones if no name is provided.
def _sync_records(index_doctype: str, test_records: dict[str, list], reset: bool = False, commit: bool = False) -> Generator[tuple[str, "Document"], None, None]:
"""Generate test objects for a register doctype from provided records."""
global test_record_manager_instance
if test_record_manager_instance is None:
test_record_manager_instance = TestRecordManager()
def _load(do_create=True):
created, loaded = [], []
for _sub_doctype, records in test_records.items():
for record in records:
if "doctype" not in record:
record["doctype"] = _sub_doctype
if do_create:
doc, was_created = _try_create(record, reset, commit)
if was_created:
created.append(doc)
else:
loaded.append(doc)
frappe.local.test_objects[index_doctype].append(MappingProxyType(record))
if not do_create:
loaded.extend(test_record_manager_instance.get_records(index_doctype))
else:
test_record_manager_instance.add(index_doctype, created)
for item in created:
yield ("created", item)
for item in loaded:
yield ("loaded", item)
if index_doctype in test_record_manager_instance.get():
if reset:
frappe.local.test_objects[index_doctype] = []
test_record_manager_instance.remove(index_doctype)
yield from _load()
else:
if index_doctype not in frappe.local.test_objects:
yield from _load(do_create=False)
else:
yield from _load()
else:
yield from _load()
The _try_create
function is responsible for creating a single document from the given record data. It uses a naming series to construct names if no name is explicitly provided. This behavior thus falls back to adding new entries rather than overwriting existing ones when regenerating records without explicit name
attributes.
def _try_create(record, reset=False, commit=False) -> tuple["Document", bool]:
"""Create a single test document from the given record data."""
def revert_naming(d):
if getattr(d, "naming_series", None):
revert_series_if_last(d.naming_series, d.name)
if not reset:
frappe.db.savepoint("creating_test_record")
d = frappe.copy_doc(record)
# Use naming series to construct names if none is provided
if d.meta.get_field("naming_series"):
if not d.naming_series:
d.naming_series = "_T-" + d.doctype + "-"
if record.get("name"):
d.name = record.get("name")
else:
d.set_new_name()
if frappe.db.exists(d.doctype, d.name) and not reset:
frappe.db.rollback(save_point="creating_test_record")
return frappe.get_doc(d.doctype, d.name), False
docstatus = d.docstatus
d.docstatus = 0
try:
d.run_method("before_test_insert")
d.insert(ignore_if_duplicate=True)
if docstatus == 1:
d.submit()
except frappe.NameError:
revert_naming(d)
except Exception as e:
if d.flags.ignore_these_exceptions_in_test and e.__class__ in d.flags.ignore_these_exceptions_in_test:
revert_naming(d)
else:
logger.debug(f"Error in making test record for {d.doctype} {d.name}")
raise
if commit:
frappe.db.commit()
return d, True
The TestRecordManager
class manages a persistent log of created test records using a JSONL file (.test_records.jsonl
). This file serves as proof of prior processing of each DocType's records and helps track creation across re-executions.
class TestRecordManager:
def __init__(self):
self.log_file = Path(frappe.get_site_path(PERSISTENT_TEST_LOG_FILE))
def get(self):
# Read log file contents
...
def add(self, index_doctype, records: list["Document"]):
# Append new entries to log file
...
def remove(self, index_doctype):
# Remove entries from log file
...
To generate test records for a specific DocType named "Customer," you can use the following code:
# Generate test records for the Customer DocType
test_records = make_test_records("Customer")
# Force regeneration of existing records
test_records_with_force = make_test_records("Customer", force=True)
# Generate and commit changes to the database
test_records_with_commit = make_test_records("Customer", commit=True)
-
Automatic Invocation: The
make_test_records
function is typically called automatically by anIntegrationTestCase
, which uses the current document as the entry point. -
Record Regeneration: When using
force=True
, existing records might not be reset but instead additional ones could be created due to naming series logic. -
Naming Series: If no
name
attribute is provided on the record then a naming series is used to construct names automatically. -
Persistent Log: The JSONL file managed by
TestRecordManager
helps track created records across tests and re-executions.
The make_test_records
function is the core load-bearing function for test record generation in Frappe's testing framework. Since it is automatically invoked by IntegrationTestCase
, manual invocation is generally unnecessary. However, understanding its components and workflow allows developers to effectively manage test data for their applications. This guide provides a comprehensive overview of this functionality while highlighting scenarios where direct invocation might be necessary—such as when recreating records after manipulation for cleanup purposes with force=True
. Additionally, the persistent JSONL log file aids in maintaining consistency across multiple runs by tracking all generated test data.