Frappe testing unit
Skill Impertio-Studio/Frappe_Claude_Skill_Package/skills/source/testing/frappe-testing-unit
60 deterministic Claude AI skills for Frappe Framework & ERPNext v14-v16 development and operations
npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-testing-unitAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
What its author says it does
Copied from the file, not written here
Use when writing unit tests, integration tests, creating test fixtures, or running tests with bench run-tests. Prevents flaky tests from missing fixtures, incorrect test isolation, and wrong test base classes. Covers frappe.tests.utils, IntegrationTestCase, UnitTestCase, test fixtures, bench run-tests flags, test naming conventions. Keywords: unit test, integration test, IntegrationTestCase, fixtures, bench run-tests, frappe.tests, test_*.py, how to write test, test fixtures, run tests, test fails, bench run-tests example..
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
12.0 KB, as published. Nobody here has run it
Unit & Integration Testing
Quick Reference
| 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 |
Decision Tree: Which Test Base Class?
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.
Test Base Classes
FrappeTestCase (v14: still works in v15+ as compatibility wrapper)
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.
UnitTestCase (v15+): No Database Access
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().
IntegrationTestCase (v15+): Full Database Access
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.
Test File Structure
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 fixtures
Rules:
- ALWAYS prefix test files with
test_— the test runner ignores files without this prefix - ALWAYS use
test_{doctype_in_snake_case}.pyfor doctype tests - NEVER place test files outside the doctype directory for doctype-specific tests
- Non-doctype tests can live in any module, but MUST follow the
test_*.pynaming
Test Fixtures
Method 1: test_records.json (Static Fixtures)
Create 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:
- ALWAYS prefix test data values with
_Testto distinguish from production data - The test runner auto-loads these before running tests for the doctype
- Link field dependencies are resolved automatically — the runner builds records for linked DocTypes first
Method 2: _test_records List (In-Module Fixtures)
_test_records = [
{"doctype": "My Doctype", "field1": "_Test Value 1"},
{"doctype": "My Doctype", "field1": "_Test Value 2"},
]
Method 3: Programmatic Fixtures (Recommended for Complex Data)
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.
Testing Patterns
Testing Document Lifecycle
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) # Cancelled
Testing Permissions
class 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")
Testing with User Context (v15+ Context Manager)
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 exits
Testing Whitelisted Methods
class 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)
Mocking External Services
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()
Testing with Settings Changes
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 reverted
Testing with Hook Overrides
class 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 Managers Reference
| 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 |
Database State Management
- IntegrationTestCase: ALWAYS rolls back database after each test — no cleanup needed
- UnitTestCase: No database connection — NEVER use
frappe.dbcalls - Each test gets a clean state: transactions start in
setUpand rollback intearDown - NEVER call
frappe.db.commit()in tests — this breaks test isolation - Use
frappe.flags.in_testto check if code is running under the test runner
Detecting Test Mode
if 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.
Common Pitfalls
- NEVER forget
super().setUpClass()— omitting this breaks fixture loading and user setup - NEVER call
frappe.db.commit()in tests — this persists data across tests and breaks isolation - ALWAYS reset user in
tearDownif you calledfrappe.set_user()directly (v14 pattern) - ALWAYS prefix test data with
_Test— makes cleanup and identification easy - NEVER rely on test execution order — each test MUST be independent
- ALWAYS use
frappe.flagsto guard fixture creation — prevents duplicate inserts
See Also
- references/examples.md — Complete test examples
- references/anti-patterns.md — Common mistakes and fixes
- references/fixtures.md — Fixture patterns in depth
- references/api-reference.md — Full API reference for test utilities
- frappe-testing-cicd — CI/CD pipeline setup