frappe-testing-unit — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited frappe-testing-unit (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Task | Command / Class |
|---|---|
| Run all tests | bench --site test_site run-tests |
| Run tests for app | bench --site test_site run-tests --app myapp |
| Run tests for doctype | bench --site test_site run-tests --doctype "Sales Order" |
| Run single test method | bench --site test_site run-tests --doctype "Sales Order" --test test_submit |
| Run tests for module | bench --site test_site run-tests --module "myapp.mymodule.doctype.mydt.test_mydt" |
| Run with profiler | bench --site test_site run-tests --doctype "Task" --profile |
| Run with failfast | bench --site test_site run-tests --failfast |
| Generate JUnit XML | bench --site test_site run-tests --junit-xml-output /path/report.xml |
| Skip fixture loading | bench --site test_site run-tests --skip-test-records --skip-before-tests |
| Base class (v14) | from frappe.tests.utils import FrappeTestCase |
| Unit test class (v15+) | from frappe.tests.classes import UnitTestCase |
| Integration test class (v15+) | from frappe.tests.classes import IntegrationTestCase |
Need to test a function or method in isolation?
├─ YES → Does it require database access?
│ ├─ NO → UnitTestCase (v15+) or FrappeTestCase (v14)
│ └─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
└─ NO → Need to test document lifecycle (create/submit/cancel)?
├─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
└─ NO → Need to test permissions or user context?
├─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
└─ NO → UnitTestCase (v15+) or FrappeTestCase (v14)Version note: In v14, FrappeTestCase is the ONLY base class. In v15+, it still works (deprecated wrapper) but ALWAYS prefer UnitTestCase or IntegrationTestCase for new code.
from frappe.tests.utils import FrappeTestCase
class TestMyDoctype(FrappeTestCase):
def test_something(self):
doc = frappe.get_doc({"doctype": "My Doctype", "field": "value"})
doc.insert()
self.assertEqual(doc.field, "value")Behavior: Resets frappe.local.flags after each test. Database transactions start before each test and rollback afterward. ALWAYS call super().setUpClass() if you override setUpClass.
from frappe.tests.classes import UnitTestCase
class TestMyUtils(UnitTestCase):
def test_calculation(self):
result = my_calculation(10, 20)
self.assertEqual(result, 30)
def test_html_output(self):
html = generate_html()
self.assertEqual(self.normalize_html(html), self.normalize_html(expected))Behavior: Sets frappe.set_user("Administrator") in setUpClass. Auto-detects doctype from module path. Provides normalize_html(), normalize_sql(), assertDocumentEqual(), assertQueryEqual(), assertSequenceSubset().
from frappe.tests.classes import IntegrationTestCase
class TestSalesOrder(IntegrationTestCase):
def test_submit_order(self):
so = frappe.get_doc({
"doctype": "Sales Order",
"customer": "_Test Customer",
"items": [{"item_code": "_Test Item", "qty": 1, "rate": 100}]
}).insert()
so.submit()
self.assertEqual(so.docstatus, 1)Behavior: Extends UnitTestCase. Calls frappe.init() and sets up site connection. Loads test record dependencies via make_test_records(). Provides primary_connection() and secondary_connection() context managers. maxDiff = 10_000.
ALWAYS place test files in the doctype directory following this naming convention:
myapp/
└── mymodule/
└── doctype/
└── my_doctype/
├── my_doctype.py # DocType controller
├── my_doctype.json # DocType definition
├── test_my_doctype.py # Test file (MUST start with test_)
└── test_records.json # Optional: test fixturesRules:
test_ — the test runner ignores files without this prefixtest_{doctype_in_snake_case}.py for doctype teststest_*.py namingCreate a test_records.json file in the doctype directory:
[
{
"doctype": "My Doctype",
"field1": "_Test Value 1",
"field2": 100
},
{
"doctype": "My Doctype",
"field1": "_Test Value 2",
"field2": 200
}
]Rules:
_Test to distinguish from production data_test_records = [
{"doctype": "My Doctype", "field1": "_Test Value 1"},
{"doctype": "My Doctype", "field1": "_Test Value 2"},
]def create_test_data():
if frappe.flags.test_data_created:
return
frappe.set_user("Administrator")
frappe.get_doc({
"doctype": "My Doctype",
"field1": "_Test Value",
}).insert()
frappe.flags.test_data_created = True
class TestMyDoctype(IntegrationTestCase):
def setUp(self):
create_test_data()ALWAYS use frappe.flags to prevent duplicate fixture creation across test methods.
class TestInvoice(IntegrationTestCase):
def test_full_lifecycle(self):
# Create
doc = frappe.get_doc({"doctype": "Sales Invoice", ...}).insert()
self.assertEqual(doc.docstatus, 0) # Draft
# Submit
doc.submit()
self.assertEqual(doc.docstatus, 1) # Submitted
# Cancel
doc.cancel()
self.assertEqual(doc.docstatus, 2) # Cancelledclass TestPermissions(IntegrationTestCase):
def test_user_cannot_read_private(self):
frappe.set_user("[email protected]")
doc = frappe.get_doc("Event", {"subject": "_Test Private Event"})
self.assertFalse(frappe.has_permission("Event", doc=doc))
def tearDown(self):
# ALWAYS reset user in tearDown
frappe.set_user("Administrator")class TestAccess(IntegrationTestCase):
def test_restricted_access(self):
with self.set_user("[email protected]"):
self.assertRaises(
frappe.PermissionError,
frappe.get_doc, "Salary Slip", "SAL-001"
)
# User automatically restored after context manager exitsclass TestAPI(IntegrationTestCase):
def test_whitelisted_method(self):
frappe.set_user("[email protected]")
result = frappe.call("myapp.api.get_dashboard_data", filters={})
self.assertIsInstance(result, dict)
self.assertIn("total", result)from unittest.mock import patch, MagicMock
class TestIntegration(IntegrationTestCase):
@patch("myapp.integrations.stripe.requests.post")
def test_payment_gateway(self, mock_post):
mock_post.return_value = MagicMock(
status_code=200,
json=lambda: {"status": "success", "id": "ch_123"}
)
result = process_payment(amount=1000, currency="USD")
self.assertEqual(result["status"], "success")
mock_post.assert_called_once()class TestFeature(IntegrationTestCase):
def test_with_modified_settings(self):
with self.change_settings("Selling Settings", {"so_required": 1}):
# Settings temporarily changed
self.assertRaises(frappe.ValidationError, create_delivery_note)
# Settings automatically revertedclass TestHooks(IntegrationTestCase):
def test_custom_hook(self):
with self.patch_hooks({"on_submit": ["myapp.hooks.custom_on_submit"]}):
doc = create_and_submit_doc()
# Verify hook was executed| Context Manager | Available On | Purpose |
|---|---|---|
set_user(user) | UnitTestCase, IntegrationTestCase | Temporarily switch user context |
change_settings(dt, **kw) | UnitTestCase, IntegrationTestCase | Temporarily modify settings |
patch_hooks(overrides) | UnitTestCase, IntegrationTestCase | Temporarily override hooks |
freeze_time(time) | UnitTestCase, IntegrationTestCase | Freeze time for deterministic tests |
debug_on(*exceptions) | UnitTestCase, IntegrationTestCase | Drop into debugger on exception |
timeout(seconds) | Decorator | Fail test if it exceeds time limit |
enable_safe_exec() | IntegrationTestCase | Enable server scripts temporarily |
switch_site(site) | IntegrationTestCase | Switch to a different site |
assertQueryCount(n) | IntegrationTestCase | Assert exact SQL query count |
assertRedisCallCounts(**kw) | IntegrationTestCase | Assert Redis command counts |
assertRowsRead(n) | IntegrationTestCase | Assert row-level DB access limits |
frappe.db callssetUp and rollback in tearDownfrappe.db.commit() in tests — this breaks test isolationfrappe.flags.in_test to check if code is running under the test runnerif frappe.flags.in_test:
# Skip external API calls, emails, etc.
return mock_response()NEVER use frappe.flags.in_test to skip validation logic — tests MUST exercise the same code paths as production.
frappe.set_user() directly (v14 pattern)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.