Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
openaec-foundation avatar

Frappe Testing Unit

  • 26 installs
  • 159 repo stars
  • Updated July 8, 2026
  • openaec-foundation/frappe_claude_skill_package

Helps with testing & qa tasks.

About

frappe-testing-unit is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted development.

  • frappe-testing-unit
  • Testing & QA
  • AI-coding skill

Frappe Testing Unit by the numbers

  • 26 all-time installs (skills.sh)
  • +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #1,382 of 2,153 Testing & QA skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/frappe_claude_skill_package --skill frappe-testing-unit

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs26
repo stars159
Last updatedJuly 8, 2026
Repositoryopenaec-foundation/frappe_claude_skill_package

What it does

Helps with testing & qa tasks.

Files

SKILL.mdMarkdownGitHub ↗

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("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 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

  • 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

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.