| name | frappe-unit-test-generator |
| description | 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. |
Frappe Unit Test Generator
Generate production-ready unit tests for Frappe applications following patterns from ERPNext and Frappe core.
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
Capabilities
1. DocType Test File Structure
Generate complete test files following Frappe's unittest framework.
Basic Test Structure (from ERPNext Item):
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")
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
2. Validation Testing
Test Controller Validations (from Sales Invoice):
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(, {
: si.items[].item_code,
: ,
:
})
si.insert()
.assertEqual((si.items), )
3. Calculation Testing
Test Amount Calculations (from Sales Invoice):
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)
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()
.assertGreater(si.total_taxes_and_charges, )
.assertEqual(
si.grand_total,
si.total + si.total_taxes_and_charges
)
4. Workflow and State Testing
Test Document States (from Stock Entry):
class TestStockEntry(FrappeTestCase):
def test_submit_workflow(self):
"""Test document submission"""
se = self._get_test_stock_entry()
se.insert()
self.assertEqual(se.docstatus, 0)
se.submit()
self.assertEqual(se.docstatus, 1)
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()
se.cancel()
self.assertEqual(se.docstatus, 2)
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()
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)
.assertEqual(amended_se.docstatus, )
5. Permission Testing
Test Role Permissions (from Frappe Core):
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)
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()
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()
.assertTrue(frappe.db.exists(, customer.name))
():
frappe.set_user(.test_user)
.assertRaises(
frappe.PermissionError,
frappe.get_doc,
,
)
():
frappe.db.exists(, .test_user):
user = frappe.get_doc({
: ,
: .test_user,
: ,
: [{: }]
})
user.insert(ignore_permissions=)
6. API Method Testing
Test Whitelisted Methods (from Frappe Core):
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")
self.assertRaises(
frappe.DoesNotExistError,
get_customer_details,
"Invalid Customer"
)
def test_api_with_filters(self):
my_app.api get_customers
frappe.set_user()
result = get_customers(filters={
:
})
.assertIsInstance(result, )
customer result:
.assertEqual(customer[], )
7. Database Query Testing
Test Database Operations:
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)
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")
)
.assertFalse(
frappe.db.exists(, )
)
():
result = frappe.db.sql(, (,), as_dict=)
.assertIsInstance(result, )
row result:
.assertIn(, row)
.assertIn(, row)
8. Child Table Testing
Test Child Table Operations (from Sales Invoice):
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()
self.assertNotEqual(si.total, original_total)
self.assertGreater(si.total, original_total)
9. Test Fixtures and Data
Create Reusable Test Data:
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"""
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()
if not frappe.db.exists("Customer", "_Test Customer"):
customer = frappe.get_doc({
"doctype": "Customer",
"customer_name": "_Test Customer",
"customer_group": ,
:
})
customer.insert()
():
doctype, name [
(, ),
(, )
]:
frappe.db.exists(doctype, name):
frappe.delete_doc(doctype, name, force=)
10. Mock and Patch Testing
Mock External Dependencies:
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")
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()
Test Organization Patterns
File Structure
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 Naming Conventions
- Test files:
test_<doctype_name>.py
- Test classes:
Test<DocTypeName>
- Test methods:
test_<description>
- Helper methods:
_<method_name>
Test Method Organization
class TestMyDocType(FrappeTestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_creation(self):
pass
def test_validation_required_fields(self):
pass
def test_total_calculation(self):
pass
def test_submit_workflow(self):
pass
def test_user_permissions(self):
pass
def _create_test_doc(self):
pass
References
Frappe Core Test Examples (Primary Reference)
Frappe Framework Tests:
ERPNext Test Examples:
Official Documentation (Secondary Reference)
Best Practices
- Test Independence: Each test should run independently
- Use setUp/tearDown: Clean state before/after each test
- Use Rollback: Call
frappe.db.rollback() in tearDown
- Test One Thing: Each test should verify one behavior
- Descriptive Names: Test names should describe what they test
- Use Assertions: Use specific assertions (
assertEqual, not just assertTrue)
- Test Edge Cases: Test boundary conditions and error cases
- Mock External Calls: Don't rely on external services in tests
- Use Test Data: Prefix test data with
_Test for easy identification
- Document Expected Behavior: Add docstrings explaining what's tested
Common Assertions
self.assertEqual(a, b)
self.assertNotEqual(a, b)
self.assertTrue(condition)
self.assertFalse(condition)
self.assertIsNone(value)
self.assertIsNotNone(value)
self.assertIn(item, collection)
self.assertNotIn(item, collection)
self.assertGreater(a, b)
self.assertLess(a, b)
self.assertGreaterEqual(a, b)
self.assertRaises(Exception, callable, *args)
with self.assertRaises(Exception):
risky_operation()
self.assertIsInstance(obj, MyClass)
Running Tests
bench --site test_site run-tests --app my_app
bench --site test_site run-tests --doctype "My DocType"
bench --site test_site run-tests --test test_my_doctype
bench --site test_site run-tests --app my_app --coverage