Frappe_Claude_Skill_Package frappe-testing-unit
install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/testing/frappe-testing-unit" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-testing-unit && rm -rf "$T"
manifest:
skills/source/testing/frappe-testing-unit/SKILL.mdsource content
Unit & Integration Testing
Quick Reference
| Task | Command / Class |
|---|---|
| Run all tests | |
| Run tests for app | |
| Run tests for doctype | |
| Run single test method | |
| Run tests for module | |
| Run with profiler | |
| Run with failfast | |
| Generate JUnit XML | |
| Skip fixture loading | |
| Base class (v14) | |
| Unit test class (v15+) | |
| Integration test class (v15+) | |
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
— the test runner ignores files without this prefixtest_ - ALWAYS use
for doctype teststest_{doctype_in_snake_case}.py - NEVER place test files outside the doctype directory for doctype-specific tests
- Non-doctype tests can live in any module, but MUST follow the
namingtest_*.py
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
to distinguish from production data_Test - 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("test1@example.com") 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("test1@example.com"): 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("test1@example.com") 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 |
|---|---|---|
| UnitTestCase, IntegrationTestCase | Temporarily switch user context |
| UnitTestCase, IntegrationTestCase | Temporarily modify settings |
| UnitTestCase, IntegrationTestCase | Temporarily override hooks |
| UnitTestCase, IntegrationTestCase | Freeze time for deterministic tests |
| UnitTestCase, IntegrationTestCase | Drop into debugger on exception |
| Decorator | Fail test if it exceeds time limit |
| IntegrationTestCase | Enable server scripts temporarily |
| IntegrationTestCase | Switch to a different site |
| IntegrationTestCase | Assert exact SQL query count |
| IntegrationTestCase | Assert Redis command counts |
| 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
callsfrappe.db - Each test gets a clean state: transactions start in
and rollback insetUptearDown - NEVER call
in tests — this breaks test isolationfrappe.db.commit() - Use
to check if code is running under the test runnerfrappe.flags.in_test
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
— omitting this breaks fixture loading and user setupsuper().setUpClass() - NEVER call
in tests — this persists data across tests and breaks isolationfrappe.db.commit() - ALWAYS reset user in
if you calledtearDown
directly (v14 pattern)frappe.set_user() - ALWAYS prefix test data with
— makes cleanup and identification easy_Test - NEVER rely on test execution order — each test MUST be independent
- ALWAYS use
to guard fixture creation — prevents duplicate insertsfrappe.flags
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