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.
Generates production-ready unit tests for Frappe DocTypes, controllers, and API methods following ERPNext patterns.
/plugin marketplace add Venkateshvenki404224/frappe-apps-manager/plugin install frappe-apps-manager@frappe-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Generate production-ready unit tests for Frappe applications following patterns from ERPNext and Frappe core.
Claude should invoke this skill when:
Generate complete test files following Frappe's unittest framework.
Basic Test Structure (from ERPNext Item):
# Pattern from: erpnext/stock/doctype/item/test_item.py
import frappe
import unittest
from frappe.tests.utils import FrappeTestCase
class TestItem(FrappeTestCase):
def setUp(self):
"""Set up test fixtures before each test"""
frappe.set_user("Administrator")
self.test_item = self._create_test_item()
def tearDown(self):
"""Clean up after each test"""
frappe.db.rollback()
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
Test Controller Validations (from Sales Invoice):
# Pattern from: erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
class TestSalesInvoice(FrappeTestCase):
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)
Test Amount Calculations (from Sales Invoice):
# Pattern from: erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
class TestSalesInvoice(FrappeTestCase):
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
)
Test Document States (from Stock Entry):
# Pattern from: erpnext/stock/doctype/stock_entry/test_stock_entry.py
class TestStockEntry(FrappeTestCase):
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)
Test Role Permissions (from Frappe Core):
# Pattern from: frappe/tests/test_permissions.py
class TestCustomerPermissions(FrappeTestCase):
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)
Test Whitelisted Methods (from Frappe Core):
# Pattern from: frappe/tests/test_api.py
class TestCustomerAPI(FrappeTestCase):
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")
Test Database Operations:
# Pattern from: frappe/tests/test_db.py
class TestCustomerQueries(FrappeTestCase):
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)
Test Child Table Operations (from Sales Invoice):
# Pattern from: erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
class TestSalesInvoiceItems(FrappeTestCase):
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):
"""Test removing items from child table"""
si = self._get_test_sales_invoice()
initial_count = len(si.items)
si.items.pop()
si.save()
self.assertEqual(len(si.items), initial_count - 1)
def test_update_item_qty(self):
"""Test updating item quantity"""
si = self._get_test_sales_invoice()
original_total = si.total
si.items[0].qty = si.items[0].qty * 2
si.save()
# Total should be recalculated
self.assertNotEqual(si.total, original_total)
self.assertGreater(si.total, original_total)
Create Reusable Test Data:
# Pattern from: erpnext/setup/doctype/company/test_company.py
class TestCompany(FrappeTestCase):
@classmethod
def setUpClass(cls):
"""Set up class-level fixtures"""
cls._create_test_data()
@classmethod
def tearDownClass(cls):
"""Clean up class-level fixtures"""
cls._cleanup_test_data()
@classmethod
def _create_test_data(cls):
"""Create test data used by multiple tests"""
# Create test company
if not frappe.db.exists("Company", "_Test Company"):
company = frappe.get_doc({
"doctype": "Company",
"company_name": "_Test Company",
"abbr": "_TC",
"default_currency": "USD",
"country": "United States"
})
company.insert()
# Create test customer
if not frappe.db.exists("Customer", "_Test Customer"):
customer = frappe.get_doc({
"doctype": "Customer",
"customer_name": "_Test Customer",
"customer_group": "Commercial",
"territory": "All Territories"
})
customer.insert()
@classmethod
def _cleanup_test_data(cls):
"""Clean up test data"""
for doctype, name in [
("Company", "_Test Company"),
("Customer", "_Test Customer")
]:
if frappe.db.exists(doctype, name):
frappe.delete_doc(doctype, name, force=True)
Mock External Dependencies:
# Pattern from: frappe/tests/test_email.py
from unittest.mock import patch, MagicMock
class TestEmailNotification(FrappeTestCase):
@patch('frappe.sendmail')
def test_send_notification(self, mock_sendmail):
"""Test email notification is sent"""
from my_app.notifications import send_welcome_email
send_welcome_email("test@example.com")
# Verify sendmail was called
mock_sendmail.assert_called_once()
args, kwargs = mock_sendmail.call_args
self.assertIn("test@example.com", kwargs["recipients"])
@patch('requests.post')
def test_external_api_call(self, mock_post):
"""Test external API integration"""
mock_post.return_value = MagicMock(
status_code=200,
json=lambda: {"success": True}
)
from my_app.integrations import sync_with_external_system
result = sync_with_external_system("_Test Customer")
self.assertTrue(result["success"])
mock_post.assert_called_once()
Standard Test File Location:
apps/my_app/
└── my_module/
└── doctype/
└── my_doctype/
├── my_doctype.py
├── my_doctype.json
├── my_doctype.js
└── test_my_doctype.py ← Test file here
test_<doctype_name>.pyTest<DocTypeName>test_<description>_<method_name>class TestMyDocType(FrappeTestCase):
# 1. Setup and teardown
def setUp(self):
pass
def tearDown(self):
pass
# 2. Creation tests
def test_creation(self):
pass
# 3. Validation tests
def test_validation_required_fields(self):
pass
# 4. Calculation tests
def test_total_calculation(self):
pass
# 5. Workflow tests
def test_submit_workflow(self):
pass
# 6. Permission tests
def test_user_permissions(self):
pass
# 7. Helper methods
def _create_test_doc(self):
pass
Frappe Framework Tests:
ERPNext Test Examples:
frappe.db.rollback() in tearDownassertEqual, not just assertTrue)_Test for easy identification# Equality checks
self.assertEqual(a, b)
self.assertNotEqual(a, b)
# Boolean checks
self.assertTrue(condition)
self.assertFalse(condition)
# Existence checks
self.assertIsNone(value)
self.assertIsNotNone(value)
# Collection checks
self.assertIn(item, collection)
self.assertNotIn(item, collection)
# Numeric comparisons
self.assertGreater(a, b)
self.assertLess(a, b)
self.assertGreaterEqual(a, b)
# Exception checks
self.assertRaises(Exception, callable, *args)
with self.assertRaises(Exception):
risky_operation()
# Type checks
self.assertIsInstance(obj, MyClass)
# Run all tests for an app
bench --site test_site run-tests --app my_app
# Run tests for specific doctype
bench --site test_site run-tests --doctype "My DocType"
# Run specific test file
bench --site test_site run-tests --test test_my_doctype
# Run with coverage
bench --site test_site run-tests --app my_app --coverage
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.