How the automated tests for Optima Payment are written, run locally, and run in CI.
Tests use Frappe's FrappeTestCase (unittest-based) and run against a live site — they
insert and submit real documents, then assert on the accounting they produce. There is no
mocking. Each test isolates itself with frappe.db.rollback() in tearDown, so nothing it
creates survives the test.
- Bank Guarantee-BG posts raw
GL Entryrows, so its tests assert GL rows (get_gle/validate_gl_entries). - Letter of Credit posts Payment Entry (Internal Transfer) documents, so its tests
assert on those Payment Entries (
get_lc_payment_entries, filtered by theis_lc_*flags). See letter-of-credit/reference.md. - Purchases Bulk Tool keeps its decisions in plain functions, so its tests come as a pair.
test_vouchers.pychecks those decisions with plain rows and writes nothing; it runs in milliseconds and needs no data on the site.test_vouchers_integration.pyasserts on the real vouchers (lines, header dates, docstatus) and the row status / reference stamps. See purchases-bulk-tool/reference.md. - Time Based Overdue Limit pairs the same way.
test_limit.pyruns the rule precedence and how triggered rules combine on plain rules;test_limit_integration.pysaves and submits real Sales Invoices and Sales Orders for a customer with an invoice 25 days overdue. See overdue-limit/reference.md.
A matrix case earns its place by naming the plausible bug it catches: how rules combine, a
boundary, a reachable edge case, or a spec table from the docs. A one-liner, a loop over the code's
own constant, and a message's wording get no case; assert which line a branch produces, not the
sentence. overdue_limit/tests/test_limit.py is the worked example.
All test fixtures come from one module: optima_payment/tests/utils.py. Import the
factories instead of building documents inline:
| Factory | Builds |
|---|---|
make_optima_payment_setting(company) |
The per-company Optima Payment Setting with all BG + LC accounts populated (back-fills a pre-existing setting) |
make_bank_guarantee_bg(bg_type=…, issue_commission=…, do_not_submit=…) |
A submitted Bank Guarantee with its full dependency chain |
make_letter_of_credit(lc_type=…, issue_commission=…, do_not_submit=…, **overrides) |
A submitted Letter of Credit; pass lc_account=None to exercise the insurance-account fallback |
make_purchases_bulk_tool(voucher_type=…, rows=…, auto_submit=…, do_not_submit=…, **overrides) |
A submitted Purchases Bulk Tool; each rows dict overrides only the keys it names (supplier, item, rate, vat, voucher_posting_date, and qty or warehouse, which default to 1 and blank) |
get_or_create_warehouse(company, warehouse_name) |
A leaf Warehouse of the company under one of its warehouse groups, for tests that need more than one |
get_or_create_purchase_taxes_template(company) |
A Purchase Taxes and Charges Template with one On Net Total row on the suite's own VAT account at PURCHASE_VAT_RATE, exclusive of the rate (included_in_print_rate: 0, whatever the site's default), with a cost center |
get_or_create_purchase_item(company, item_code, is_stock_item=…) |
A purchase Item taxed by the suite's own Item Tax Template, its tax row carrying the test suppliers' tax category so that template applies; pass is_stock_item=1 for Purchase Receipt tests |
get_or_create_purchase_item_tax_template(company) |
The Item Tax Template behind those items, on the same account and rate |
set_test_cost_center_default(company) |
Makes a leaf cost center the session user's cost_center default, so the tool's vouchers carry one on sites that make it mandatory on Purchase Invoice. It lives in the defaults cache: pair it with frappe.clear_cache(user=frappe.session.user) after the rollback |
make_sales_order(customer=…, do_not_insert=…, do_not_submit=…, **overrides) |
A one-line Sales Order on the test item, cost center and pre-seeded tax row; make_reference_sales_order wraps it |
make_sales_invoice(customer=…, posting_date=…, due_date=…, do_not_insert=…, do_not_submit=…, **overrides) |
A one-line Sales Invoice; a past posting_date is kept, and the due date defaults to it. The header has no cost center, as on real invoices, because a payment copies it onto balance sheet lines that some sites refuse one on |
make_invoice_overdue(invoice, days) |
Moves the invoice's due date and payment schedule days into the past, then runs ERPNext's update_invoice_status so the invoice turns Overdue as it would on a live site |
make_credit_note(invoice) |
An unsaved full return of the invoice, with the return reason some sites require |
make_invoice_payment(invoice, allocate=…) |
A submitted Payment Entry for the invoice's outstanding amount; allocate=False leaves it unallocated, so the invoice stays outstanding while the customer owes nothing |
make_overdue_limit(party=…, action=…, threshold_days=…, applies_to=…, company=…, exempt_roles=…, enabled=…) |
A Time Based Overdue Limit rule |
The factories are defensive (get_or_create_*) and work around this bench's KSA/ZATCA
customizations (mandatory Arabic name fields, tax category, item tax templates, order-level
tax rows). They default to erpnext.get_default_company().
Always pass the site you are working on (this bench hosts several):
# One module (recommended — this is what CI runs)
bench --site <site> run-tests --module optima_payment.optima_payment.doctype.letter_of_credit.test_letter_of_credit
bench --site <site> run-tests --module optima_payment.optima_payment.doctype.bank_guarantee_bg.test_bank_guarantee_bg
bench --site <site> run-tests --module optima_payment.purchases_bulk.tests.test_vouchers
bench --site <site> run-tests --module optima_payment.purchases_bulk.tests.test_vouchers_integration
bench --site <site> run-tests --module optima_payment.overdue_limit.tests.test_limit
bench --site <site> run-tests --module optima_payment.overdue_limit.tests.test_limit_integrationThe site needs optima_payment installed and allow_tests enabled
(bench --site <site> set-config allow_tests true).
Run these suites with --module. The --doctype "Letter of Credit" form makes Frappe
auto-generate test records for the doctype's entire link dependency graph, which pulls in
ERPNext's standard fixtures (e.g. _Test Company) and fails on a site that doesn't have
that full test dataset:
frappe.exceptions.LinkValidationError: Could not find Company: _Test Company
Our suites don't use those fixtures — they build their own data with the factories in
tests/utils.py against the site's real default company — so --module runs them directly
and avoids the dependency machinery.
The suites target standard ERPNext and are not tied to any customization app. They build
their own data with create_*/make_*-style factories against the site's real default
company and isolate with db.rollback() — the same integration-test pattern ERPNext itself
uses (see erpnext/accounts/test_utils.py). A test must never change behaviour based on
whether a specific third-party/customer app is installed.
Because Optima Payment is a Saudi-first app, its real (dev and customer) sites carry
KSA/ZATCA doctype-level customizations — e.g. mandatory customer_name_in_arabic,
tax_category, and item tax templates. The factory helpers in tests/utils.py therefore
stay tolerant of these: they satisfy the extra mandatory fields when present and fall
back gracefully when absent (unknown keys are dropped; missing groups fall back). This is
deliberate — the same suite then runs on a clean CI site and on a customized KSA site,
which is the environment the app actually ships into. Do not strip these workarounds to
"purify" the factory: that would leave a suite that only runs on vanilla ERPNext, the
environment least like production.
On a clean CI site (erpnext + optima_payment only) these KSA paths are inert rather than
exercised. If you want CI to mirror production and genuinely exercise them, install
optima_zatca (and any other KSA customization apps) in .github/workflows/ci.yml so the
mandatory-field paths are hit for real. Until then, treat the KSA branches in the factory as
covered only on real sites. See the TODO block in test_letter_of_credit.py.
CI is defined in .github/workflows/ci.yml and runs on every push / pull request against
version-15. It spins up MariaDB + Redis, installs erpnext + optima_payment on a fresh
test_site, enables tests, and then auto-discovers every test_*.py under the
optima_payment/ package via rglob, running bench run-tests --module … for each.
You do not need to register new test files anywhere — dropping a
test_<something>.py under the package (including doctype-local …/doctype/<x>/test_<x>.py)
is enough for CI to pick it up on the next run.