界面测试

你可以使用 Cypress 编写 UI 测试。它是一个基于 NodeJS 的
全栈测试框架,不依赖 Selenium。

要编写集成测试,请在 cypress/integration
目录中创建一个 .js 文件。

示例

以下是一个用于检查待办事项插入的集成测试示例

context('ToDo', () => {
 before(() => {
 cy.login('Administrator', 'admin');
 cy.visit('/desk');
 });

 it('creates a new todo', () => {
 cy.visit('/app/todo/new-todo-1');
 cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
 cy.get('.page-title').should('contain', 'Not Saved');
 cy.get('.primary-action').click();
 cy.visit('/desk#List/ToDo');
 cy.location('hash').should('eq', '/app/todo');
 cy.get('.list-row').should('contain', 'this is a test todo');
 });
});

在本地运行 Cypress

Cypress 使用你系统上安装的任意基于 Chromium 的浏览器来运行测试。
每个应用都有自己的 Cypress 测试套件。要运行某个应用的测试,请在 frappe-bench 目录中运行
以下命令。

bench --site [sitename] run-ui-tests [app]

这将打开 Cypress Electron 界面,你可以在其中手动运行任意测试,或
运行所有测试。

你也可以在无头模式下运行测试。

# run in headless mode
bench --site [sitename] run-ui-tests [app] --headless

要启用 Cypress 并行测试,你可以传入 --parallel 标志。
有关 Cypress 并行测试工作原理的更多信息可以在此处找到。

# run tests parallelly
bench --site [sitename] run-ui-tests [app] --parallel

代码覆盖率

代码覆盖率有助于识别测试期间执行了源代码的哪些行。为了测量代码覆盖率,需要对源代码进行插桩,并且需要将此插桩后的源代码与我们的测试运行器集成,以收集覆盖率并生成报告。

对于 Frappe 中的 Cypress 测试,.js 文件使用 Istanbul 进行插桩,并使用 Cypress 代码覆盖率插件合并每个测试的覆盖率并保存合并结果。

代码插桩:

为了计算源代码的哪些行被执行了,会通过插桩向代码中插入额外的计数器。例如:

插桩前:

function foo(a, b) {
 if (a < b)
 return b - a;
 else
 return a - b;
}

插桩后:

cov_1m1jljnmzu();

function foo(a, b) {
 cov_1m1jljnmzu().f[0]++;
 cov_1m1jljnmzu().s[0]++;

 if (a < b) {
 cov_1m1jljnmzu().b[0][0]++;
 cov_1m1jljnmzu().s[1]++;
 return b - a;
 } else {
 cov_1m1jljnmzu().b[0][1]++;
 cov_1m1jljnmzu().s[2]++;
 return a - b;
 }
}

当使用这个修改过(插桩)的源代码进行测试时,这些计数器会随着代码的执行而递增,并生成一个覆盖率对象。然后 Cypress 代码覆盖率插件会处理收集到的覆盖率并生成覆盖率报告。

在本地生成代码覆盖率报告:

  1. 使用 istanbul/nyc 对源代码进行插桩:
npx nyc instrument -x 'frappe/public/dist/**' -x 'frappe/public/js/lib/**' -x '**/*.bundle.js' --compact=false --in-place frappe

这会将 frappe 文件夹中的现有源代码替换为插桩后的源代码。-x 标志用于排除指定的路径。你也可以使用 -n 标志来指定要包含的路径。有关 nyc instrument 命令的更多详细信息,请参见此处

  1. 运行 Cypress 测试:
bench --site test_site run-ui-tests frappe --with-coverage
  1. 生成报告:
npx nyc report --reporter=text

有关其他报告格式,请参见此处

Testing-Library 查询

你也可以在 Cypress 测试中使用 Testing Library 查询。Testing Library 提供了以下测试工具:

  • 让编写模拟用户与应用交互方式的 UI 测试变得更加容易
  • 让在不需要了解所有实现细节的情况下查找 DOM 中的元素变得更加容易
  • 帮助保持测试的可维护性(这样小的重构就不会破坏测试)

有关用法的更多详细信息,请参见 Testing Library 文档

Testing Library 提供了多种查询来查找页面上的元素。以下是一些示例:

  • ByRole

在此处查看 HTML 元素及其默认角色的表格

查询 元素
findByRole('button', {name: 'Save'}) “ 且可访问名称为 ‘Save’
findByRole('checkbox') “
findByRole('textbox') , (也匹配其他默认 role=’textbox’ 的元素)
findByRole('searchbox') <input type="search">
findByRole('listbox') <select></select>, “
  • ByLabelText
查询 元素
findByLabelText('Optimize') 与标签 ‘Optimize’ 关联的元素
  • ByPlaceholderText
查询 元素
findByPlaceholderText('Name') placeholder=’Name’ 的元素
  • ByText
查询 元素
findByText('example.json') textContent=’example.json’ 的元素
  • ByDisplayValue
查询 元素
findByDisplayValue('Option 1') 且选中的 <option></option> 为 ‘Option 1’ (也匹配值属性相符的 <input> 或 <textarea></textarea>)
  • ByTitle
查询 元素
findByTitle('Open Link') title=’Open Link’ 的元素
  • ByAltText
  • ByTestId

测试

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 分钟。到那时,第二个实例能够执行其他测试文件,这有助于平衡构建之间的时间。