mcpField
F
frappe-testing-unit

frappe-testing-unit

github.com/impertio-studio测试与质量198115 次安装30 周前更新
代码仓库

关于 frappe-testing-unit

面向 Frappe 开发者的单元与集成测试参考技能,介绍不同版本的测试类、夹具、测试模式和状态管理,并提供运行与生成报告的命令。

主要功能

  1. 01单元与集成测试类
  2. 02测试运行命令与选项
  3. 03夹具配置模式
  4. 04文档生命周期与权限测试
  5. 05管理测试状态的上下文管理器
  6. 06数据库隔离指引

适用场景

  1. 01测试独立函数和方法
  2. 02验证文档生命周期行为
  3. 03检查权限与用户上下文
  4. 04运行应用测试并生成报告

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-testing-unit

安装后在 Claude Code 中输入/frappe-testing-unit确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
.gitkeep
anti-patterns.md
api-reference.md
examples.md
fixtures.md

详细介绍

单元测试与集成测试

快速参考

任务命令 / 类
运行所有测试bench --site test_site run-tests
运行应用的测试bench --site test_site run-tests --app myapp
运行 DocType 的测试bench --site test_site run-tests --doctype "Sales Order"
运行单个测试方法bench --site test_site run-tests --doctype "Sales Order" --test test_submit
运行模块的测试bench --site test_site run-tests --module "myapp.mymodule.doctype.mydt.test_mydt"
使用分析器运行bench --site test_site run-tests --doctype "Task" --profile
使用 failfast 运行bench --site test_site run-tests --failfast
生成 JUnit XMLbench --site test_site run-tests --junit-xml-output /path/report.xml
跳过 Fixture 加载bench --site test_site run-tests --skip-test-records --skip-before-tests
基类(v14)from frappe.tests.utils import FrappeTestCase
单元测试类(v15+)from frappe.tests.classes import UnitTestCase
集成测试类(v15+)from frappe.tests.classes import IntegrationTestCase

决策树:选择哪个测试基类?

Need to test a function or method in isolation?
├─ YES → Does it require database access?
│   ├─ NO → UnitTestCase (v15+) or FrappeTestCase (v14)
│   └─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
└─ NO → Need to test document lifecycle (create/submit/cancel)?
    ├─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
    └─ NO → Need to test permissions or user context?
        ├─ YES → IntegrationTestCase (v15+) or FrappeTestCase (v14)
        └─ NO → UnitTestCase (v15+) or FrappeTestCase (v14)

版本说明:在 v14 中,FrappeTestCase 是唯一的基类。在 v15+ 中,它仍然可用(弃用的包装类),但对于新代码,始终优先使用 UnitTestCase 或 IntegrationTestCase。

测试基类

FrappeTestCase(v14:在 v15+ 中仍可作为兼容性包装类使用)
from frappe.tests.utils import FrappeTestCase

class TestMyDoctype(FrappeTestCase):
    def test_something(self):
        doc = frappe.get_doc({"doctype": "My Doctype", "field": "value"})
        doc.insert()
        self.assertEqual(doc.field, "value")

行为:每次测试后重置 frappe.local.flags。数据库事务在每次测试前启动,并在测试后回滚。如果重写 setUpClass,始终调用 super().setUpClass()。

UnitTestCase(v15+):不访问数据库
from frappe.tests.classes import UnitTestCase

class TestMyUtils(UnitTestCase):
    def test_calculation(self):
        result = my_calculation(10, 20)
        self.assertEqual(result, 30)

    def test_html_output(self):
        html = generate_html()
        self.assertEqual(self.normalize_html(html), self.normalize_html(expected))

行为:在 setUpClass 中设置 frappe.set_user("Administrator")。根据模块路径自动检测 DocType。提供 normalize_html()、normalize_sql()、assertDocumentEqual()、assertQueryEqual()、assertSequenceSubset()。

IntegrationTestCase(v15+):完整数据库访问权限
from frappe.tests.classes import IntegrationTestCase

class TestSalesOrder(IntegrationTestCase):
    def test_submit_order(self):
        so = frappe.get_doc({
            "doctype": "Sales Order",
            "customer": "_Test Customer",
            "items": [{"item_code": "_Test Item", "qty": 1, "rate": 100}]
        }).insert()
        so.submit()
        self.assertEqual(so.docstatus, 1)

行为:扩展 UnitTestCase。调用 frappe.init() 并建立站点连接。通过 make_test_records() 加载测试记录依赖项。提供 primary_connection() 和 secondary_connection() 上下文管理器。maxDiff = 10_000

测试文件结构

始终按照以下命名约定,将测试文件放在 DocType 目录中:

myapp/
└── mymodule/
    └── doctype/
        └── my_doctype/
            ├── my_doctype.py          # DocType controller
            ├── my_doctype.json        # DocType definition
            ├── test_my_doctype.py     # Test file (MUST start with test_)
            └── test_records.json      # Optional: test fixtures

规则:

  • 测试文件名始终以 test_ 开头——测试运行器会忽略不带此前缀的文件
  • DocType 测试始终使用 test_{doctype_in_snake_case}.py
  • DocType 专属测试文件绝不能放在 DocType 目录之外
  • 非 DocType 测试可以放在任何模块中,但必须遵循 test_*.py 命名规则

测试 Fixture

方法 1:test_records.json(静态 Fixture)

在 DocType 目录中创建 test_records.json 文件:

[
    {
        "doctype": "My Doctype",
        "field1": "_Test Value 1",
        "field2": 100
    },
    {
        "doctype": "My Doctype",
        "field1": "_Test Value 2",
        "field2": 200
    }
]

规则:

  • 测试数据值始终以 _Test 开头,以便与生产数据区分
  • 测试运行器会在运行该 DocType 的测试前自动加载这些数据
  • 链接字段依赖项会自动解析——运行器会先为关联的 DocType 创建记录
方法 2:_test_records 列表(模块内 Fixture)
_test_records = [
    {"doctype": "My Doctype", "field1": "_Test Value 1"},
    {"doctype": "My Doctype", "field1": "_Test Value 2"},
]
方法 3:程序化 Fixture(复杂数据推荐使用)
def create_test_data():
    if frappe.flags.test_data_created:
        return
    frappe.set_user("Administrator")
    frappe.get_doc({
        "doctype": "My Doctype",
        "field1": "_Test Value",
    }).insert()
    frappe.flags.test_data_created = True

class TestMyDoctype(IntegrationTestCase):
    def setUp(self):
        create_test_data()

始终使用 frappe.flags,以防止在多个测试方法中重复创建 Fixture。

测试模式

测试文档生命周期
class TestInvoice(IntegrationTestCase):
    def test_full_lifecycle(self):
        # Create
        doc = frappe.get_doc({"doctype": "Sales Invoice", ...}).insert()
        self.assertEqual(doc.docstatus, 0)  # Draft

        # Submit
        doc.submit()
        self.assertEqual(doc.docstatus, 1)  # Submitted

        # Cancel
        doc.cancel()
        self.assertEqual(doc.docstatus, 2)  # Cancelled
测试权限
class TestPermissions(IntegrationTestCase):
    def test_user_cannot_read_private(self):
        frappe.set_user("[email protected]")
        doc = frappe.get_doc("Event", {"subject": "_Test Private Event"})
        self.assertFalse(frappe.has_permission("Event", doc=doc))

    def tearDown(self):
        # ALWAYS reset user in tearDown
        frappe.set_user("Administrator")
使用用户上下文进行测试(v15+ 上下文管理器)
class TestAccess(IntegrationTestCase):
    def test_restricted_access(self):
        with self.set_user("[email protected]"):
            self.assertRaises(
                frappe.PermissionError,
                frappe.get_doc, "Salary Slip", "SAL-001"
            )
        # User automatically restored after context manager exits
测试白名单方法
class TestAPI(IntegrationTestCase):
    def test_whitelisted_method(self):
        frappe.set_user("[email protected]")
        result = frappe.call("myapp.api.get_dashboard_data", filters={})
        self.assertIsInstance(result, dict)
        self.assertIn("total", result)
模拟外部服务
from unittest.mock import patch, MagicMock

class TestIntegration(IntegrationTestCase):
    @patch("myapp.integrations.stripe.requests.post")
    def test_payment_gateway(self, mock_post):
        mock_post.return_value = MagicMock(
            status_code=200,
            json=lambda: {"status": "success", "id": "ch_123"}
        )
        result = process_payment(amount=1000, currency="USD")
        self.assertEqual(result["status"], "success")
        mock_post.assert_called_once()
使用设置变更进行测试
class TestFeature(IntegrationTestCase):
    def test_with_modified_settings(self):
        with self.change_settings("Selling Settings", {"so_required": 1}):
            # Settings temporarily changed
            self.assertRaises(frappe.ValidationError, create_delivery_note)
        # Settings automatically reverted
使用 Hook 覆盖进行测试
class TestHooks(IntegrationTestCase):
    def test_custom_hook(self):
        with self.patch_hooks({"on_submit": ["myapp.hooks.custom_on_submit"]}):
            doc = create_and_submit_doc()
            # Verify hook was executed

上下文管理器参考

上下文管理器适用对象用途
set_user(user)UnitTestCase、IntegrationTestCase临时切换用户上下文
change_settings(dt, **kw)UnitTestCase、IntegrationTestCase临时修改设置
patch_hooks(overrides)UnitTestCase、IntegrationTestCase临时覆盖钩子
freeze_time(time)UnitTestCase、IntegrationTestCase冻结时间,以便进行确定性测试
debug_on(*exceptions)UnitTestCase、IntegrationTestCase发生异常时进入调试器
timeout(seconds)装饰器测试超出时间限制时使其失败
enable_safe_exec()IntegrationTestCase临时启用服务器脚本
switch_site(site)IntegrationTestCase切换到其他站点
assertQueryCount(n)IntegrationTestCase断言 SQL 查询数量完全一致
assertRedisCallCounts(**kw)IntegrationTestCase断言 Redis 命令数量
assertRowsRead(n)IntegrationTestCase断言行级数据库访问限制

数据库状态管理

  • IntegrationTestCase:每次测试后始终回滚数据库——无需清理
  • UnitTestCase:没有数据库连接——绝不要使用 frappe.db 调用
  • 每个测试都会获得干净的状态:事务在 setUp 中开始,并在 tearDown 中回滚
  • 绝不要在测试中调用 frappe.db.commit()——这会破坏测试隔离
  • 使用 frappe.flags.in_test 检查代码是否在测试运行器下运行

检测测试模式

if frappe.flags.in_test:
    # Skip external API calls, emails, etc.
    return mock_response()

绝不要使用 frappe.flags.in_test 跳过验证逻辑——测试必须执行与生产环境相同的代码路径。

常见陷阱

  1. 绝不要忘记 super().setUpClass()——省略此项会导致夹具加载和用户设置失败
  2. 绝不要在测试中调用 frappe.db.commit()——这会使数据在测试之间持久化,并破坏隔离
  3. 如果直接调用了 frappe.set_user(),务必在 tearDown 中重置用户(v14 模式)
  4. 务必为测试数据添加 _Test 前缀——便于清理和识别
  5. 绝不要依赖测试执行顺序——每项测试都必须相互独立
  6. 务必使用 frappe.flags 防止重复创建夹具

另请参阅

评论

支持 Markdown