Claude
Skills
Sign in
Back

frappe-unit-test-generator

Included with Lifetime
$97 forever

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.

Backend & APIs

What this skill does


# 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 <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/<app>/<app>/<module>/doctype/<name>/<name>.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 <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 <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 <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_<doctype>.py`; test classes are `Test<DocTypeName>`; 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()

    

Related in Backend & APIs