AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Frappe Unit Test Generator

skill-venkateshvenki404224-frappe-apps-manager-frappe-unit-test-generator · by Venkateshvenki404224

Generate comprehensive unit tests for Frappe DocTypes, controllers, and API methods. Use when creating test files, writing test cases, or setting up test infrastructure for Frappe/ERPNext applications.

No reviews yet
0 installs
28 views
0.0% view→install

Install

$ agentstack add skill-venkateshvenki404224-frappe-apps-manager-frappe-unit-test-generator

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-venkateshvenki404224-frappe-apps-manager-frappe-unit-test-generator)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Frappe Unit Test Generator? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Frappe Unit Test Generator

Generate production-ready unit tests for Frappe applications following patterns from ERPNext and Frappe core.

Global Rules

These Frappe conventions apply to everything this skill generates, and override any conflicting example below.

  • Bench commands: use bare bench (never ./env/bin/bench or a full path). Always pass --site explicitly — never run a bare bench migrate / bench run-tests. Run bench start in the background and only if it isn't already running. Don't run discovery commands (which bench, bench --version).
  • DocType files live at apps////doctype//.json — the app name appears twice (directory + Python package) — with an empty __init__.py alongside. Never mkdir the folder; write the JSON and run bench --site migrate to create the structure. Don't add creation, modified, owner, modified_by, or docstatus as fields — Frappe manages them.
  • Database & ORM: prefer frappe.qb.get_query() over raw frappe.db.sql(). Use frappe.db.get_all() for server logic (ignores permissions) and frappe.db.get_list() for user-facing APIs (enforces them). Never use frappe.db.set_value() on a field with validation or lifecycle logic — load the doc and doc.save() so controller hooks run. Batch-fetch related records; never query inside a loop (N+1).
  • Never call frappe.db.commit() in controllers, request handlers, background jobs, or patches — Frappe auto-commits on success and rolls back on uncaught errors. Flush manually only to make a write visible to a subsequent frappe.enqueue() (or pass enqueue_after_commit=True).
  • Permissions & APIs: put permission checks inside controller methods (enforced on every call path), not in API wrappers. Type-hint every @frappe.whitelist() parameter so Frappe validates and casts it, and pass methods=[...] to pin the HTTP verb.

When to Use This Skill

Claude should invoke this skill when:

  • User wants to write unit tests for DocTypes
  • User needs to test controller methods
  • User requests API endpoint tests
  • User wants to test business logic or validations
  • User mentions testing, test cases, or test files
  • User wants to set up test fixtures or test data
  • User needs to test permissions or workflows

Test Site

Run tests on a separate site from the one the user is actively developing on. Tests create, modify, and delete data — running them on the dev site will pollute it.

Convention: if the dev site is app.localhost, create app-test.localhost for tests and install the app there:

bench new-site app-test.localhost --admin-password admin
bench --site app-test.localhost install-app 

Always run tests against the test site, and always pass --site — never run a bare bench run-tests:

bench --site app-test.localhost run-tests --app 

If tests fail with "DocType not found", run bench --site app-test.localhost migrate first.

Choosing a Base Class

  • DB-backed tests (anything that creates/reads documents) inherit from frappe.tests.IntegrationTestCasenot unittest.TestCase. Tests run inside a transaction that automatically rolls back, so no manual cleanup or frappe.db.rollback() is needed.
  • Pure-logic tests (utility functions, calculations, parsing — no DB or Frappe context) inherit from frappe.tests.UnitTestCase. It skips DB setup/teardown, so it is faster.
  • Test files are named test_.py; test classes are Test; each test method starts with test_.
  • Test expected exceptions with self.assertRaises(frappe.ValidationError, doc.insert).

Capabilities

1. DocType Test File Structure

Generate complete test files following Frappe's testing framework.

Basic Test Structure (DB-backed, uses IntegrationTestCase):

import frappe
from frappe.tests import IntegrationTestCase

class TestItem(IntegrationTestCase):
    def setUp(self):
        """Set up test fixtures before each test (transaction auto-rolls-back)"""
        frappe.set_user("Administrator")
        self.test_item = self._create_test_item()

    def test_item_creation(self):
        """Test basic item creation"""
        item = frappe.get_doc({
            "doctype": "Item",
            "item_code": "_Test Item",
            "item_name": "Test Item",
            "item_group": "Products",
            "stock_uom": "Nos"
        })
        item.insert()

        self.assertEqual(item.item_code, "_Test Item")
        self.assertEqual(item.item_group, "Products")

        # Verify item was created
        self.assertTrue(frappe.db.exists("Item", "_Test Item"))

    def _create_test_item(self):
        """Helper method to create test item"""
        if frappe.db.exists("Item", "_Test Item"):
            return frappe.get_doc("Item", "_Test Item")

        item = frappe.get_doc({
            "doctype": "Item",
            "item_code": "_Test Item",
            "item_name": "Test Item",
            "item_group": "Products",
            "stock_uom": "Nos",
            "is_stock_item": 1
        })
        item.insert()
        return item

Pure-Logic Test (no DB, uses UnitTestCase):

from frappe.tests import UnitTestCase
from my_app.utils import calculate_tax

class TestTaxUtils(UnitTestCase):
    def test_calculate_tax(self):
        """Pure calculation — no Frappe context or database needed"""
        self.assertEqual(calculate_tax(100, 0.1), 10)

2. Validation Testing

Test Controller Validations (from Sales Invoice):

# Pattern from: erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
class TestSalesInvoice(IntegrationTestCase):
    def test_posting_date_validation(self):
        """Test posting date cannot be future date"""
        si = self._get_test_sales_invoice()
        si.posting_date = frappe.utils.add_days(frappe.utils.today(), 1)

        self.assertRaises(frappe.ValidationError, si.insert)

    def test_items_required(self):
        """Test that items are required"""
        si = frappe.get_doc({
            "doctype": "Sales Invoice",
            "customer": "_Test Customer",
            "items": []
        })

        self.assertRaises(frappe.ValidationError, si.insert)

    def test_negative_quantity(self):
        """Test negative quantities are not allowed"""
        si = self._get_test_sales_invoice()
        si.items[0].qty = -1

        with self.assertRaises(frappe.ValidationError) as context:
            si.insert()

        self.assertIn("Quantity cannot be negative", str(context.exception))

    def test_duplicate_items(self):
        """Test duplicate items with same item code"""
        si = self._get_test_sales_invoice()
        si.append("items", {
            "item_code": si.items[0].item_code,
            "qty": 5,
            "rate": 100
        })

        # Depending on requirements, this might succeed or fail
        # Document the expected behavior
        si.insert()
        self.assertEqual(len(si.items), 2)

3. Calculation Testing

Test Amount Calculations (from Sales Invoice):

# Pattern from: erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
class TestSalesInvoice(IntegrationTestCase):
    def test_total_calculation(self):
        """Test total amount calculation"""
        si = frappe.get_doc({
            "doctype": "Sales Invoice",
            "customer": "_Test Customer",
            "items": [{
                "item_code": "_Test Item",
                "qty": 10,
                "rate": 100
            }, {
                "item_code": "_Test Item 2",
                "qty": 5,
                "rate": 50
            }]
        })
        si.insert()

        self.assertEqual(si.total, 1250)  # (10*100) + (5*50)

    def test_discount_calculation(self):
        """Test discount application"""
        si = self._get_test_sales_invoice()
        si.discount_amount = 100
        si.save()

        expected_total = si.total - 100
        self.assertEqual(si.grand_total, expected_total)

    def test_tax_calculation(self):
        """Test tax calculation with tax template"""
        si = self._get_test_sales_invoice()
        si.taxes_and_charges = "_Test Tax Template"
        si.save()

        # Tax amount should be calculated
        self.assertGreater(si.total_taxes_and_charges, 0)
        self.assertEqual(
            si.grand_total,
            si.total + si.total_taxes_and_charges
        )

4. Workflow and State Testing

Test Document States (from Stock Entry):

# Pattern from: erpnext/stock/doctype/stock_entry/test_stock_entry.py
class TestStockEntry(IntegrationTestCase):
    def test_submit_workflow(self):
        """Test document submission"""
        se = self._get_test_stock_entry()
        se.insert()

        # Verify draft state
        self.assertEqual(se.docstatus, 0)

        # Submit and verify
        se.submit()
        self.assertEqual(se.docstatus, 1)

        # Verify cannot edit submitted doc
        se.purpose = "Different Purpose"
        self.assertRaises(frappe.ValidationError, se.save)

    def test_cancel_workflow(self):
        """Test document cancellation"""
        se = self._get_test_stock_entry()
        se.insert()
        se.submit()

        # Cancel and verify
        se.cancel()
        self.assertEqual(se.docstatus, 2)

        # Verify cancelled doc cannot be submitted again
        self.assertRaises(frappe.ValidationError, se.submit)

    def test_amendment(self):
        """Test document amendment after cancellation"""
        se = self._get_test_stock_entry()
        se.insert()
        se.submit()
        se.cancel()

        # Create amended document
        amended_se = frappe.copy_doc(se)
        amended_se.amended_from = se.name
        amended_se.docstatus = 0
        amended_se.insert()
        amended_se.submit()

        self.assertEqual(amended_se.amended_from, se.name)
        self.assertEqual(amended_se.docstatus, 1)

5. Permission Testing

Test Role Permissions (from Frappe Core):

# Pattern from: frappe/tests/test_permissions.py
class TestCustomerPermissions(IntegrationTestCase):
    def setUp(self):
        self.test_user = "test@example.com"
        self._setup_test_user()

    def test_read_permission(self):
        """Test user can read allowed documents"""
        frappe.set_user(self.test_user)

        # Should succeed
        customer = frappe.get_doc("Customer", "_Test Customer")
        self.assertEqual(customer.name, "_Test Customer")

    def test_write_permission(self):
        """Test user can edit allowed documents"""
        frappe.set_user(self.test_user)

        customer = frappe.get_doc("Customer", "_Test Customer")
        customer.customer_name = "Updated Name"
        customer.save()

        # Verify change persisted
        customer.reload()
        self.assertEqual(customer.customer_name, "Updated Name")

    def test_create_permission(self):
        """Test user can create new documents"""
        frappe.set_user(self.test_user)

        customer = frappe.get_doc({
            "doctype": "Customer",
            "customer_name": "New Customer"
        })
        customer.insert()

        self.assertTrue(frappe.db.exists("Customer", customer.name))

    def test_denied_access(self):
        """Test user cannot access restricted documents"""
        frappe.set_user(self.test_user)

        # Should raise PermissionError
        self.assertRaises(
            frappe.PermissionError,
            frappe.get_doc,
            "Customer",
            "_Restricted Customer"
        )

    def _setup_test_user(self):
        """Create test user with specific roles"""
        if not frappe.db.exists("User", self.test_user):
            user = frappe.get_doc({
                "doctype": "User",
                "email": self.test_user,
                "first_name": "Test",
                "roles": [{"role": "Sales User"}]
            })
            user.insert(ignore_permissions=True)

6. API Method Testing

Test Whitelisted Methods (from Frappe Core):

# Pattern from: frappe/tests/test_api.py
class TestCustomerAPI(IntegrationTestCase):
    def test_get_customer_details(self):
        """Test API method returns correct data"""
        from my_app.api import get_customer_details

        frappe.set_user("Administrator")
        result = get_customer_details("_Test Customer")

        self.assertIsNotNone(result)
        self.assertEqual(result["name"], "_Test Customer")
        self.assertIn("customer_group", result)

    def test_api_authentication(self):
        """Test API requires authentication"""
        frappe.set_user("Guest")

        from my_app.api import get_customer_details

        self.assertRaises(
            frappe.PermissionError,
            get_customer_details,
            "_Test Customer"
        )

    def test_api_validation(self):
        """Test API validates input parameters"""
        from my_app.api import get_customer_details

        frappe.set_user("Administrator")

        # Test with invalid customer
        self.assertRaises(
            frappe.DoesNotExistError,
            get_customer_details,
            "Invalid Customer"
        )

    def test_api_with_filters(self):
        """Test API method with filter parameters"""
        from my_app.api import get_customers

        frappe.set_user("Administrator")
        result = get_customers(filters={
            "customer_group": "Commercial"
        })

        self.assertIsInstance(result, list)
        for customer in result:
            self.assertEqual(customer["customer_group"], "Commercial")

7. Database Query Testing

Test Database Operations:

# Pattern from: frappe/tests/test_db.py
class TestCustomerQueries(IntegrationTestCase):
    def test_get_all_with_filters(self):
        """Test frappe.get_all with filters"""
        customers = frappe.get_all(
            "Customer",
            filters={"customer_group": "Commercial"},
            fields=["name", "customer_name"]
        )

        self.assertIsInstance(customers, list)
        self.assertGreater(len(customers), 0)

        # Verify all results match filter
        for customer in customers:
            doc = frappe.get_doc("Customer", customer.name)
            self.assertEqual(doc.customer_group, "Commercial")

    def test_get_value(self):
        """Test frappe.db.get_value"""
        customer_group = frappe.db.get_value(
            "Customer",
            "_Test Customer",
            "customer_group"
        )

        self.assertIsNotNone(customer_group)
        self.assertIsInstance(customer_group, str)

    def test_exists(self):
        """Test frappe.db.exists"""
        self.assertTrue(
            frappe.db.exists("Customer", "_Test Customer")
        )
        self.assertFalse(
            frappe.db.exists("Customer", "Non Existent Customer")
        )

    def test_sql_query(self):
        """Test raw SQL queries"""
        result = frappe.db.sql("""
            SELECT name, customer_name
            FROM `tabCustomer`
            WHERE customer_group = %s
            LIMIT 10
        """, ("Commercial",), as_dict=True)

        self.assertIsInstance(result, list)
        for row in result:
            self.assertIn("name", row)
            self.assertIn("customer_name", row)

8. Child Table Testing

Test Child Table Operations (from Sales Invoice):

# Pattern from: erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
class TestSalesInvoiceItems(IntegrationTestCase):
    def test_add_items(self):
        """Test adding items to child table"""
        si = self._get_test_sales_invoice()
        initial_count = len(si.items)

        si.append("items", {
            "item_code": "_Test Item 2",
            "qty": 5,
            "rate": 150
        })
        si.save()

        self.assertEqual(len(si.items), initial_count + 1)

    def test_remove_items(self):

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Venkateshvenki404224](https://github.com/Venkateshvenki404224)
- **Source:** [Venkateshvenki404224/frappe-apps-manager](https://github.com/Venkateshvenki404224/frappe-apps-manager)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.