# Frappe Unit Test Generator

> 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.

- **Type:** Skill
- **Install:** `agentstack add skill-venkateshvenki404224-frappe-apps-manager-frappe-unit-test-generator`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Venkateshvenki404224](https://agentstack.voostack.com/s/venkateshvenki404224)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Venkateshvenki404224](https://github.com/Venkateshvenki404224)
- **Source:** https://github.com/Venkateshvenki404224/frappe-apps-manager/tree/main/frappe-apps-manager/skills/frappe-unit-test-generator

## Install

```sh
agentstack add skill-venkateshvenki404224-frappe-apps-manager-frappe-unit-test-generator
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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:
```bash
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`:
```bash
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.IntegrationTestCase` — **not** `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`):
```python
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`):
```python
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):
```python
# 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):
```python
# 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):
```python
# 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):
```python
# 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):
```python
# 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**:
```python
# 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):
```python
# 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.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-venkateshvenki404224-frappe-apps-manager-frappe-unit-test-generator
- Seller: https://agentstack.voostack.com/s/venkateshvenki404224
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
