agentsclimarketplace

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

Install
npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-testing-unit

Assembled 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

TaskCommand / Class
Run all testsbench --site test_site run-tests
Run tests for appbench --site test_site run-tests --app myapp
Run tests for doctypebench --site test_site run-tests --doctype "Sales Order"
Run single test methodbench --site test_site run-tests --doctype "Sales Order" --test test_submit
Run tests for modulebench --site test_site run-tests --module "myapp.mymodule.doctype.mydt.test_mydt"
Run with profilerbench --site test_site run-tests --doctype "Task" --profile
Run with failfastbench --site test_site run-tests --failfast
Generate JUnit XMLbench --site test_site run-tests --junit-xml-output /path/report.xml
Skip fixture loadingbench --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}.py for 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_*.py naming

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 _Test to 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 ManagerAvailable OnPurpose
set_user(user)UnitTestCase, IntegrationTestCaseTemporarily switch user context
change_settings(dt, **kw)UnitTestCase, IntegrationTestCaseTemporarily modify settings
patch_hooks(overrides)UnitTestCase, IntegrationTestCaseTemporarily override hooks
freeze_time(time)UnitTestCase, IntegrationTestCaseFreeze time for deterministic tests
debug_on(*exceptions)UnitTestCase, IntegrationTestCaseDrop into debugger on exception
timeout(seconds)DecoratorFail test if it exceeds time limit
enable_safe_exec()IntegrationTestCaseEnable server scripts temporarily
switch_site(site)IntegrationTestCaseSwitch to a different site
assertQueryCount(n)IntegrationTestCaseAssert exact SQL query count
assertRedisCallCounts(**kw)IntegrationTestCaseAssert Redis command counts
assertRowsRead(n)IntegrationTestCaseAssert 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.db calls
  • Each test gets a clean state: transactions start in setUp and rollback in tearDown
  • NEVER call frappe.db.commit() in tests — this breaks test isolation
  • Use frappe.flags.in_test to 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

  1. NEVER forget super().setUpClass() — omitting this breaks fixture loading and user setup
  2. NEVER call frappe.db.commit() in tests — this persists data across tests and breaks isolation
  3. ALWAYS reset user in tearDown if you called frappe.set_user() directly (v14 pattern)
  4. ALWAYS prefix test data with _Test — makes cleanup and identification easy
  5. NEVER rely on test execution order — each test MUST be independent
  6. ALWAYS use frappe.flags to guard fixture creation — prevents duplicate inserts

See Also

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.