Frappe 提供了一些基本工具来编写自动化测试。以下是一些基本规则:
- 测试可以放在仓库中的任何位置,但必须以
test_ 开头,并且应该是一个 .py 文件。
- 测试运行器会自动为依赖的 DocType 构建测试记录,这些 DocType 由
Link 类型字段(外键)标识。
- 对于非 DocType 测试,你可以编写简单的单元测试,并在文件名前加上
test_。
编写测试
当你创建一个新的 DocType(在开发者模式下)时,样板文件中也包含 test_{doctype}.py 文件。测试文件应该负责创建依赖项并清理它们。
以下是一个参考自 test_event.py 的示例测试文件。
import frappe
import unittest
def create_events():
if frappe.flags.test_events_created:
return
frappe.set_user("Administrator")
doc = frappe.get_doc({
"doctype": "Event",
"subject":"_Test Event 1",
"starts_on": "2014-01-01",
"event_type": "Public"
}).insert()
doc = frappe.get_doc({
"doctype": "Event",
"subject":"_Test Event 3",
"starts_on": "2014-01-01",
"event_type": "Public"
"event_individuals": [{
"person": "test1@example.com"
}]
}).insert()
frappe.flags.test_events_created = True
class TestEvent(unittest.TestCase):
def setUp(self):
create_events()
def tearDown(self):
frappe.set_user("Administrator")
def test_allowed_public(self):
frappe.set_user("test1@example.com")
doc = frappe.get_doc("Event", frappe.db.get_value("Event",
{"subject":"_Test Event 1"}))
self.assertTrue(frappe.has_permission("Event", doc=doc))
def test_not_allowed_private(self):
frappe.set_user("test1@example.com")
doc = frappe.get_doc("Event", frappe.db.get_value("Event",
{"subject":"_Test Event 2"}))
self.assertFalse(frappe.has_permission("Event", doc=doc))
为命令编写测试
要为你的 Bench 命令编写测试,你可以将测试分组到一个类中,该类继承自 frappe.tests.test_commands 中的 BaseTestCommands 和 unittest.TestCase,以便它在 bench run-tests 命令期间运行。
作为参考,以下是为 bench execute 命令编写的一些测试。
class TestCommands(BaseTestCommands, unittest.TestCase):
def test_execute(self):
# test 1: execute a command expecting a numeric output
self.execute("bench --site {site} execute frappe.db.get_database_size")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(float(self.stdout), float)
# test 2: execute a command expecting an errored output as local won't exist
self.execute("bench --site {site} execute frappe.local.site")
self.assertEqual(self.returncode, 1)
self.assertIsNotNone(self.stderr)
# test 3: execute a command with kwargs
# Note:
# terminal command has been escaped to avoid .format string replacement
# The returned value has quotes which have been trimmed for the test
self.execute("""bench --site {site} execute frappe.bold --kwargs '{{"text": "DocType"}}'""")
self.assertEqual(self.returncode, 0)
self.assertEqual(self.stdout[1:-1], frappe.bold(text='DocType'))
运行测试
运行测试可能需要应用在其 dev-requirements.txt 文件中指定的额外依赖项。在运行测试之前,请确保所有应用都已使用 bench setup requirements --dev 安装了开发依赖项。
运行以下命令来运行所有测试。它会先构建所有测试依赖项一次,然后运行你的测试。你应该从 frappe_bench 文件夹中运行测试。
# run all tests
bench --site [sitename] run-tests
# run tests for only frappe app
bench --site [sitename] run-tests --app frappe
# run tests for the Task doctype
bench --site [sitename] run-tests --doctype "Task"
# run tests for All doctypes in specified Module Def
bench --site [sitename] run-tests --module-def "Contacts"
# run a test using module path
bench --site [sitename] run-tests --module frappe.tests.test_api
# run a specific test from a test file
bench --site [sitename] run-tests --module frappe.tests.test_api --test test_insert_many
# run tests without creating test records
bench --site [sitename] run-tests --skip-test-records --doctype "Task"
# profile tests and show a report after tests execute
bench --site [sitename] run-tests --profile --doctype "Task"
.
----------------------------------------------------------------------
Ran 1 test in 0.010s
OK
9133 function calls (8912 primitive calls) in 0.011 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert)
1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate)
13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql)
255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get)
12 0.000 0.000 0.002 0.000
# verbose log level for tests
bench --site [sitename] --verbose run-tests
检查测试是否正在运行
你可以通过评估 frappe.in_test 全局变量来检查你的代码是否作为测试的一部分被执行。在较旧的框架版本中,你可以改为评估 frappe.flags.in_test(这将在即将发布的版本中被弃用)。
你可以使用该全局变量根据测试是否正在运行来在代码中添加分支。例如:
if not frappe.in_test:
notify_error_to_user()
并行运行测试
随着项目中测试数量的增长,如果在一台机器上串行运行,测试完成需要很长时间。
在多台测试机器上并行运行测试可以在持续集成(CI)中节省时间。
并行测试
命令:
bench --site [sitename] --app [app-name] run-parallel-tests --build-id <build-number> --total-build <total-number-of-builds>
用法:
如果你想在 2 个 CI 实例上运行测试,你的命令如下:
# in first CI instance
bench --site [sitename] run-parallel-tests --build-id 1 --total-builds 2
# in second CI instance
bench --site [sitename] run-parallel-tests --build-id 2 --total-builds 2
注意:该命令会将所有测试文件分成 2 部分,并在这些 CI 实例中执行它们。
测试列表的前半部分将在第一个实例中执行,测试列表的后半部分将在第二个实例中执行。
使用编排器进行并行测试
可能会出现每个测试完成所需时间不同的情况,这可能导致 CI 构建之间的时间不均衡。为了缓解这个问题,你可以使用测试编排器,它根据 CI 实例的可用性来运行下一个测试。使用测试编排器进行并行测试的命令如下。
命令:
bench --site [sitename] --app [app-name] run-parallel-tests --use-orchestrator
用法:
如果你想在 2 个 CI 实例上运行测试,你的命令如下
# in first CI instance
bench --site [sitename] run-parallel-tests --use-orchestrator
# in second CI instance
bench --site [sitename] run-parallel-tests --use-orchestrator
注意:此命令需要环境变量 CI_BUILD_ID 和 ORCHESTRATOR_URL。CI_BUILD_ID 是你在每次 CI 构建运行时获得的唯一 ID。
ORCHESTRATOR_URL 是你托管编排器后获得的公开可访问 URL。
对比
为了清楚地了解上述并行测试命令变体的工作方式,请查看以下示例。
假设有 4 个测试文件如下
test_module_one.py 4 mins (execution time)
test_module_two.py 2 mins
test_module_three.py 1 min
test_module_four.py 1 min
不使用并行测试命令所需的时间。
test_module_one.py 4 mins
test_module_two.py 2 mins
test_module_three.py 1 min
test_module_four.py 1 min
==============================
Total Wait Time 8 mins
使用第一个命令将测试文件自动拆分到 2 个测试实例所需的时间。
# First instance # Second instance
test_module_one.py 4 mins test_module_three.py 1 min
test_module_two.py 2 mins test_module_four.py 1 min
---------------------------- ----------------------------
6 mins 2 mins
==============================
Total Wait Time 6 mins
使用第二个命令(使用编排器根据 2 个测试实例的可用性运行测试)所需的时间可能如下。
# First instance # Second instance
test_module_one.py 4 mins test_module_two.py 2 mins
---------------------------- test_module_three.py 1 mins
4 mins test_module_four.py 1 min
----------------------------
4 mins
==============================
Total Wait Time 4 mins
注意:第一个实例上只执行了一个测试文件,因为它忙碌了 4 分钟。到那时,第二个实例能够执行其他测试文件,这有助于平衡构建之间的时间。