$npx -y skills add lubusIN/frappe-skills --skill frappe-testingWrite and run tests for Frappe apps including unit tests, integration tests, and UI tests. Use when adding test coverage, debugging test failures, or setting up CI for Frappe projects.
| 1 | # Frappe Testing |
| 2 | |
| 3 | Write and run tests for Frappe applications using the built-in testing framework. |
| 4 | |
| 5 | ## When to use |
| 6 | |
| 7 | - Writing unit tests for DocType controllers |
| 8 | - Writing integration tests for workflows and APIs |
| 9 | - Running existing test suites |
| 10 | - Debugging test failures |
| 11 | - Setting up CI pipelines for Frappe apps |
| 12 | |
| 13 | ## Inputs required |
| 14 | |
| 15 | - App name to test |
| 16 | - Site name for test execution |
| 17 | - Specific module/DocType to test (optional) |
| 18 | - Test environment (dev site, dedicated test site) |
| 19 | |
| 20 | ## Procedure |
| 21 | |
| 22 | ### 0) Setup test environment |
| 23 | |
| 24 | ```bash |
| 25 | # Install dev dependencies |
| 26 | bench setup requirements --dev |
| 27 | |
| 28 | # Ensure site is ready |
| 29 | bench --site <site> migrate |
| 30 | ``` |
| 31 | |
| 32 | ### 1) Run tests |
| 33 | |
| 34 | ```bash |
| 35 | # Run all tests for an app |
| 36 | bench --site <site> run-tests --app my_app |
| 37 | |
| 38 | # Run tests for specific module |
| 39 | bench --site <site> run-tests --module my_app.my_module.tests |
| 40 | |
| 41 | # Run tests for specific DocType |
| 42 | bench --site <site> run-tests --doctype "My DocType" |
| 43 | |
| 44 | # Verbose output |
| 45 | bench --site <site> run-tests --app my_app -v |
| 46 | |
| 47 | # Run single test file |
| 48 | bench --site <site> run-tests --module my_app.doctype.sample_doc.test_sample_doc |
| 49 | ``` |
| 50 | |
| 51 | ### 2) Write DocType tests |
| 52 | |
| 53 | Create `test_<doctype_name>.py` alongside the DocType: |
| 54 | |
| 55 | ```python |
| 56 | # my_app/doctype/sample_doc/test_sample_doc.py |
| 57 | import frappe |
| 58 | from frappe.tests.utils import FrappeTestCase |
| 59 | |
| 60 | class TestSampleDoc(FrappeTestCase): |
| 61 | def setUp(self): |
| 62 | # Create test data |
| 63 | self.doc = frappe.get_doc({ |
| 64 | "doctype": "Sample Doc", |
| 65 | "title": "Test Document" |
| 66 | }).insert() |
| 67 | |
| 68 | def tearDown(self): |
| 69 | # Cleanup |
| 70 | frappe.delete_doc("Sample Doc", self.doc.name, force=True) |
| 71 | |
| 72 | def test_creation(self): |
| 73 | self.assertEqual(self.doc.title, "Test Document") |
| 74 | |
| 75 | def test_validation(self): |
| 76 | doc = frappe.get_doc({ |
| 77 | "doctype": "Sample Doc", |
| 78 | "title": "" # Invalid - required field |
| 79 | }) |
| 80 | self.assertRaises(frappe.ValidationError, doc.insert) |
| 81 | |
| 82 | def test_workflow(self): |
| 83 | self.doc.status = "Approved" |
| 84 | self.doc.save() |
| 85 | self.assertEqual(self.doc.status, "Approved") |
| 86 | ``` |
| 87 | |
| 88 | ### 3) Write API tests |
| 89 | |
| 90 | ```python |
| 91 | # my_app/tests/test_api.py |
| 92 | import frappe |
| 93 | from frappe.tests.utils import FrappeTestCase |
| 94 | |
| 95 | class TestAPI(FrappeTestCase): |
| 96 | def test_whitelist_method(self): |
| 97 | from my_app.api import process_order |
| 98 | |
| 99 | # Create test order |
| 100 | order = frappe.get_doc({ |
| 101 | "doctype": "Sales Order", |
| 102 | "customer": "_Test Customer" |
| 103 | }).insert() |
| 104 | |
| 105 | # Test the API |
| 106 | result = process_order(order.name, "approve") |
| 107 | |
| 108 | self.assertEqual(result["status"], "success") |
| 109 | |
| 110 | # Cleanup |
| 111 | frappe.delete_doc("Sales Order", order.name, force=True) |
| 112 | |
| 113 | def test_permission_denied(self): |
| 114 | # Test as restricted user |
| 115 | frappe.set_user("guest@example.com") |
| 116 | |
| 117 | from my_app.api import sensitive_action |
| 118 | self.assertRaises(frappe.PermissionError, sensitive_action, "doc-001") |
| 119 | |
| 120 | # Reset user |
| 121 | frappe.set_user("Administrator") |
| 122 | ``` |
| 123 | |
| 124 | ### 4) Test permissions |
| 125 | |
| 126 | ```python |
| 127 | def test_role_permissions(self): |
| 128 | # Create user with specific role |
| 129 | user = frappe.get_doc({ |
| 130 | "doctype": "User", |
| 131 | "email": "test_user@example.com", |
| 132 | "roles": [{"role": "Sales User"}] |
| 133 | }).insert() |
| 134 | |
| 135 | frappe.set_user("test_user@example.com") |
| 136 | |
| 137 | # Test permission |
| 138 | self.assertTrue(frappe.has_permission("Sales Order", "read")) |
| 139 | self.assertFalse(frappe.has_permission("Sales Order", "delete")) |
| 140 | |
| 141 | # Cleanup |
| 142 | frappe.set_user("Administrator") |
| 143 | frappe.delete_doc("User", user.name, force=True) |
| 144 | ``` |
| 145 | |
| 146 | ### 5) Use fixtures |
| 147 | |
| 148 | ```python |
| 149 | # my_app/doctype/sample_doc/test_records.json |
| 150 | [ |
| 151 | { |
| 152 | "doctype": "Sample Doc", |
| 153 | "title": "Test Record 1" |
| 154 | }, |
| 155 | { |
| 156 | "doctype": "Sample Doc", |
| 157 | "title": "Test Record 2" |
| 158 | } |
| 159 | ] |
| 160 | ``` |
| 161 | |
| 162 | Reference in tests: |
| 163 | ```python |
| 164 | class TestSampleDoc(FrappeTestCase): |
| 165 | def test_fixture_loaded(self): |
| 166 | doc = frappe.get_doc("Sample Doc", "Test Record 1") |
| 167 | self.assertIsNotNone(doc) |
| 168 | ``` |
| 169 | |
| 170 | ### 6) Run UI tests (Cypress) |
| 171 | |
| 172 | ```bash |
| 173 | # Run UI tests for an app |
| 174 | bench --site <site> run-ui-tests my_app |
| 175 | |
| 176 | # Headless mode |
| 177 | bench --site <site> run-ui-tests my_app --headless |
| 178 | ``` |
| 179 | |
| 180 | ## Verification |
| 181 | |
| 182 | - [ ] All tests pass: `bench --site <site> run-tests --app my_app` |
| 183 | - [ ] No test pollution (tests are isolated) |
| 184 | - [ ] Tests run in < 5 minutes for fast feedback |
| 185 | - [ ] CI pipeline runs tests on each commit |
| 186 | |
| 187 | ## Failure modes / debugging |
| 188 | |
| 189 | - **Test not found**: Ensure filename starts with `test_` and class/method names follow conventions |
| 190 | - **Database errors**: Tests may not be isolated—check for missing cleanup |
| 191 | - **Permission errors in tests**: Use `frappe.set_u |