测试

Frappe 提供了一些基本工具来编写自动化测试。以下是一些基本规则:

  1. 测试可以放在仓库中的任何位置,但必须以 test_ 开头,并且应该是一个 .py 文件。
  2. 测试运行器会自动为依赖的 DocType 构建测试记录,这些 DocType 由 Link 类型字段(外键)标识。
  3. 对于非 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 分钟。到那时,第二个实例能够执行其他测试文件,这有助于平衡构建之间的时间。