- 后端
- 企业应用
【免费下载链接】iTop
A simple, web based CMDB & IT Service Management tool
本指南面向 iTop 开发者,系统讲解如何为 iTop 创建、维护 PHPUnit 单元测试与集成测试。文章基于仓库内 tests/php-unit-tests/README.md 官方文档,并结合phpunit.xml.dist配置、ItopTestCase/ItopDataTestCase/ItopCustomDatamodelTestCase基类源码与真实测试用例,覆盖环境准备、测试基类选型、命名规范、常用断言技巧、性能调优以及 PHPUnit 进程隔离机制。读完本文,你将能够独立搭建 iTop 测试环境、编写符合项目规范的测试用例,并解决单进程运行带来的静态污染与隔离问题。
1. 概览:iTop 的测试体系
iTop(IT Operations Portal)的 PHP 测试体系全部集中在 tests/php-unit-tests 目录下,按目录结构天然划分为两大类别:
unitary-tests/(单元测试):覆盖某个 iTop PHP 类或方法的行为。例如 unitary-tests/core/UserRightsTest.php 测试用户权限判断逻辑,unitary-tests/core/OQLParserTest.php 测试 OQL 解析器。integration-tests/(集成测试):覆盖数据或配置在应用层面的整体一致性。例如 integration-tests/DictionariesConsistencyTest.php 检查各语言字典的一致性,integration-tests/DetectStaticPollutionTest.php 检测测试代码中的静态污染模式。
除上述两类外,目录中还包含legacy-tests/(历史遗留测试脚本)、experiments/(PHPUnit 机制验证实验,见 experiments/README.md)以及src/BaseTestCase/(测试基类源码)等辅助区域。
2. 环境搭建:前置条件与依赖安装
2.1 PHPUnit 配置文件
仓库提供了默认的 PHPUnit 配置:phpunit.xml.dist。该文件是版本受控的默认配置,不要直接修改。如需个性化配置(例如调整内存限制、增加测试套件),请复制为phpunit.xml(不入库),PHPUnit 会优先加载它:
cp tests/php-unit-tests/phpunit.xml.dist tests/php-unit-tests/phpunit.xml从默认配置可以看出 iTop 测试套件的组织方式:Application、Core、Datamodels、Setup、Sources、Synchro、Webservices共七个单元测试套件,外加Extensions(扫描env-production/*/test与env-production/*/tests/php-unit-tests)和Integration两个套件;bootstrap指向 unittestautoload.php,它同时加载测试目录下的 Composer autoload(vendor/autoload.php)与 iTop 自身的 lib/autoload.php,以保证可以继承 Symfony KernelTestCase 并调用 iTop 核心类。
仓库还提供另外两份专项配置:module_integration.xml.dist 用于验证模块/扩展(N°6949),postbuild_integration.xml.dist 用于构建后的集成验证,两者均可按同样方式复制为对应
.xml后使用。
2.2 PHP 运行环境配置
由于 iTop 的大部分测试在同一个 PHP 进程中连续执行,以下配置直接关系到能否跑完整个套件:
memory_limit:单进程跑全部测试时内存消耗会持续累积,务必保证足够的内存上限。默认配置已在 phpunit.xml.dist 的<php>段设置为512M,可根据本机情况调大。- CLI php.ini:
- 开启 OpCache:加速 iTop 类文件的重复加载;
- 关闭 Xdebug(
xdebug.mode=off):官方文档指出,关闭 Xdebug 可带来2 到 3 倍的性能提升,且仍可在 PHPStorm 中调试。
基类源码也内置了防呆提醒:ItopTestCase.php 的
setUpBeforeClass()会在交互式运行检测到 Xdebug 已启用时打印警告,并提示去php.ini调整。
2.3 测试依赖安装
iTop 本体依赖(lib/)随仓库一起分发,但测试自身的依赖不随仓库分发,需要手动安装。在tests/php-unit-tests目录下执行:
cd tests/php-unit-tests && composer install依据 composer.json,测试依赖为phpunit/phpunit ^9与sempro/phpunit-pretty-print ^1.4(用于美化输出)。同时该文件通过 PSR-4 将Combodo\iTop\Test\UnitTest\命名空间映射到src/BaseTestCase/、src/Service/,因此测试类可直接use基类。
2.4 被测 iTop 实例的安装要求
要运行数据相关的测试套件,需要先安装一个满足以下选项的 iTop 实例(默认安装选项):
- 配置管理(Configuration Management):全部勾选;
- Service Management for Enterprises;
- Simple Ticket Management + Customer portal;
- Simple Change Management;
- 额外勾选 Additional ITIL tickets 中的Known Errors Management与FAQ。
这些模块为ItopDataTestCase中的构造辅助方法(如CreateTicket、CreateObjectWithTagSet、CreateServer)提供了UserRequest、FAQ、Server等数据类与默认组织,缺了会导致对应测试无法通过。
3. 测试基类选型:三个层级与使用原则
iTop 提供三个继承自 PHPUnitTestCase的基类,能力逐级递增:
| 基类 | 适用场景 | 关键能力 |
|---|---|---|
\Combodo\iTop\Test\UnitTest\ItopTestCase | 不需要访问数据模型与数据库的简单测试 | 加载 iTop 核心类、可选启动 Symfony Kernel、提供大量反射与临时文件辅助方法 |
\Combodo\iTop\Test\UnitTest\ItopDataTestCase | 需要访问 iTop 元模型(metamodel)与数据库的测试 | 自动启动 MetaModel、默认以事务包裹 CRUD、测试结束自动回滚 |
\Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase | 需要非标准数据模型(自定义 XML delta)的测试 | 在独立环境中编译应用自定义 XML delta(iTop 2.7.9 / 3.0.4 / 3.1.0 引入,N°6097) |
3.1 ItopTestCase:最轻量的基类
ItopTestCase.php 继承自 Symfony 的KernelTestCase,其setUpBeforeClass()负责加载 approot.inc.php、定义ITOP_PHPUNIT_RUNNING_CONSTANT_NAME并设置 Portal 环境变量;tearDownAfterClass()负责把utils::GetConfig()强制重载回磁盘配置并重置用户语言。此外它还提供了大量实用辅助方法:
RequireOnceItopFile()/RequireOnceUnitTestFile()(自 2.7.9 / 3.0.3 / 3.1.0 起,N°5608):手动加载不在 Composer autoload 范围内的 iTop 类或测试 Mock 类;InvokeNonPublicMethod()/InvokeNonPublicStaticMethod()/GetNonPublicProperty()/SetNonPublicProperty():通过反射调用/读写私有方法或属性;BackupStaticProperties()/RestoreStaticProperties()(3.2.0):备份并还原类静态属性;GetTemporaryFilePath():申请一个会被tearDown()自动清理的临时文件;CanonicalizeXML()/AssertEqualiTopXML():规范化 XML 后做比对,用于数据模型 delta 类断言;AssertArraysHaveSameItems()(3.2.0):忽略数组顺序的相等断言。
3.2 ItopDataTestCase:数据与事务管理
ItopDataTestCase.php 继承ItopTestCase,通过setUp()调用MetaModel::Startup()启动元模型,并且默认(USE_TRANSACTION = true)在每次测试前执行START TRANSACTION,测试结束后ROLLBACK,从而保证 CRUD 操作不留痕、测试之间互不干扰。tearDown()还会执行UserRights::Logoff()注销用户,并复位 CMDB 变更上下文与事件监听器。
该基类内置了一整套数据工厂方法,可直接在测试中构造业务对象:
$this->CreateOrganization('MyOrg'); // 创建组织 $this->CreateTicket(1); // 创建编号 Ticket_1 的 UserRequest $this->CreateServer(1); // 创建 Server_1 $this->CreatePerson(1); // 创建 Person_1 $this->CreateUser('admin_test', 1); // 创建指定 profile 的本地用户典型用法见 unitary-tests/core/UserRightsTest.php:测试类继承ItopDataTestCase后,通过UserRights::Login($sLogin)切换用户上下文来断言权限判断结果。它还提供assertDBQueryCount()(断言一段操作触发的 MySQL 查询条数)与assertDBChangeOpCount()(断言对象产生的 CMDBChangeOp 条数)等专用断言。
3.3 ItopCustomDatamodelTestCase:自定义数据模型
ItopCustomDatamodelTestCase.php 继承ItopDataTestCase,用于测试需要标准数据模型中不存在的类、属性、枚举值等场景。用法分两步:
- 继承该类并重写
GetDatamodelDeltaAbsPath()返回 XML delta 的绝对路径; - 按普通方式编写测试方法。
真实示例:DisplayBlockTest.php 通过 delta 文件Delta/add-enum-value-with-quote.xml为UserRequest.status枚举增加带引号的新值,用来验证图表渲染时的转义处理。
底层机制上,其PrepareEnvironment()会在名为php-unit-tests的独立环境中编译所有派生测试类声明的 delta,并切换到独立数据库(原库名_phpunittests),避免污染生产库;多个兼容的 delta 会合并进同一环境以节省编译时间。由于该类会临时修改 MetaModel/环境,其构造函数强制setRunClassInSeparateProcess(true),保证测试类在专属进程中运行。辅助实现见 src/Service/UnitTestRunTimeEnvironment.php,它会自动扫描各测试目录并发现实现了GetDatamodelDeltaAbsPath的测试类。
3.4 命名规范
iTop 测试严格遵循 PHPUnit 官方约定(PHPUnit Manual – Writing Tests for PHPUnit):
- 被测类
MyClass对应测试类MyClassTest; - 被测方法
MyMethod对应测试方法testMyMethod。
3.5 基类选型原则(性能优先)
官方建议按"最简优先"的顺序选择基类:
- 优先继承 PHPUnit 原生
TestCase; - 需要 iTop 核心类时再继承
ItopTestCase; - 只有确实需要访问数据模型/数据库时才使用
ItopDataTestCase。
因为数据类测试每次都要启动 MetaModel 并开启事务,成本显著高于纯单元测试,能用更轻的基类解决问题就不要升级。
4. 编写测试:通用 PHPUnit 技巧
4.1 临时跳过测试
仅用于临时禁用的测试(例如修复尚未合入时先提交的用例),调用:
$this->markTestSkipped('explanation');仓库中的实际用法包括依赖模块未安装、依赖的修复未合入(如 DisplayBlockTest.php 等待 N°7313)、环境不支持(如getallheaders()不可用,见 LoginExternalTest.php)等场景。
对于"数据提供器中某些组合不适用"这类永久性情况,不要使用 skip,应通过
assertTrue(true)后return让用例正常通过。
4.2 断言异常
在调用会抛出异常的代码之前调用expectException,并可叠加expectExceptionMessage或expectExceptionMessageMatches:
// 尝试删除 tag,必须报错! $this->expectException(DeleteException::class); $this->expectExceptionMessageMatches('/'.$this->GetKey().'/'); $oTagData->DBDelete();⚠️ 警告:一旦条件满足,测试立即结束,其后的代码将不再执行!若还需在异常后进行更多断言,应改用 try/catch 方式:
$validator = new FormValidator(); try { $validator->validate( new DateTimeImmutable('2020-01-01'), new DateTimeImmutable('1999-01-01'), -3, '' ); $this->fail('FormValidationException was not thrown'); } catch (AssertionFailedError $e) { throw $e; // 处理上面 fail() 的调用 } catch (FormValidationException $e) { $this->assertSame( [ 'End must be after start', 'The new id must be greater than 0', 'Description can not be empty', ], $e->getErrors() ); }注意:try/catch 中必须重抛AssertionFailedError,否则fail()造成的失败会被吞掉。仓库中大量用例(如 utilsTest.php)均采用expectException+expectExceptionMessage组合。
5. 编写测试:iTop 专项技巧
5.1 加载 autoloader 之外的类
- 继承
ItopDataTestCase的测试会自动获得全部 iTop 类(Composer autoload + iTop autoload,含已安装模块); - 继承
ItopTestCase的测试如果需要加载 Composer autoload 之外的 iTop 类,可重写LoadRequiredItopFiles(),在内部调用:RequireOnceItopFile('core/attributedef.class.inc.php'):按相对 iTop 根目录的路径加载;RequireOnceUnitTestFile('./WeeklyScheduledProcessMockConfig.php'):按相对当前调用文件的路径加载 Mock 类。
这两个方法自 2.7.9 / 3.0.3 / 3.1.0(N°5608)引入,避免了测试目录在 iTop 结构中移动时绝对路径失效的问题。
5.2 添加用户上下文
需要以特定用户身份测试权限或数据可见性时,使用UserRights::Login():
UserRights::Login('admin_test');数据类基类的tearDown()会在每个用例结束后自动执行UserRights::Logoff(),因此不同用例间的用户状态不会互相泄漏。
6. 测试性能优化
6.1 内存限制
由于测试在单进程内连续运行,内存会随用例累积。可在 XML 配置的 PHP 段调整:
<ini name="memory_limit" value="512M"/>6.2 度量单个用例耗时
在测试函数内多处粘贴下面这行,即可输出各段代码的执行耗时:
if (isset($fStarted)) {echo 'L'.__LINE__.': '.round(microtime(true) - $fStarted, 3)."\n";} $fStarted = microtime(true);6.3 选择合适的隔离配置(针对 ItopDataTestCase)
当使用ItopDataTestCase时,按以下步骤确定是否需要进程隔离:
- 先把测试类写绿,保持无进程隔离运行;
- 运行整个
unitary-tests套件; - 若出现仅在全量运行时才复现的"假阳性",再开始排查,并优先考虑静态污染问题。
6.4 保持环境干净:静态污染检测
由于单进程内静态变量会跨用例保留,测试代码若直接修改全局静态状态,就会污染后续用例。仓库提供专门的静态分析测试 integration-tests/DetectStaticPollutionTest.php(@runClassInSeparateProcess),扫描unitary-tests下所有 PHP 文件,发现以下污染模式即失败:
| 检测到的模式 | 建议替代方案 |
|---|---|
ContextTag::addTag() | 改用new ContextTag() |
EventService::RegisterListener() | 改用ItopDataTestCase::EventService_RegisterListener()(框架自动注销) |
Dict::Add() | 等待实现 ItopDataTestCase 的门面方法 |
以下模式不会污染环境,因为测试框架已自动处理:
Configuration:每个测试类执行后自动重置(见 ItopTestCase.php 的tearDownAfterClass(),强制从磁盘重载配置);UserRights:每个用例后自动logoff;Dict::SetUserLanguage:每个用例后自动重置用户语言。
此外可借助@beforeClass/@afterClass在类级别执行清理。若测试无论如何都会污染环境,最后的手段才是为它开启进程隔离(见下一节)。
7. PHPUnit 进程隔离机制
7.1 为什么要隔离:理解测试间交互
默认情况下,PHPUnit 在单进程中顺序执行所有测试,第一个用例对静态状态、全局变量、数据库连接的改动可能影响后续用例。排查方法:在 PHPStorm 中同时选中两个测试,右键 →run,观察第一个测试是否影响第二个的执行结果。
7.2 进程隔离的成本
隔离是把双刃剑——每个用例都要在新进程中重新引导环境:
- 隔离一个极简单的
TestCase:约4 ms; - 隔离一个
ItopDataTestCase:约800 ms(需重新启动 MetaModel、开启事务等)。
因此隔离必须精准使用,而不是全局开启。
7.3 三种隔离粒度
① 方法级(推荐):在测试方法的 docblock 加注解@runInSeparateProcess,仅该用例在独立进程运行。仓库示例:CMDBSourceTest.php中重置数据库连接、DeprecatedCallsLogErrorHandlerTest.php中避免常量冲突的用例均使用此注解。
注意:自 3.0.4 / 3.1.1 / 3.2.0(N°6658)起,无需再附加
@backupGlobals disabled与@preserveGlobalState disabled——前者本来就是默认值,后者已由ItopTestCase通过类属性protected $preserveGlobalState = false;全局设置(见 ItopTestCase.php)。老测试中残留的这两行注解可以移除。
② 类级:在类 docblock 加注解@runTestsInSeparateProcesses,该类的每个用例都独立运行。仓库示例:RestServicesTest.php、RestServicesSanitizeOutputTest.php、apcEmulationTest.php(需要 mock MetaModel 与 utils)均使用此注解。
③ 全局(绝对不要做):把processIsolation写入 phpunit.xml.dist ——默认配置中processIsolation="false",请保持它不变。全局隔离会让整个套件耗时爆炸,绝大多数用例根本不需要隔离。
7.4 进一步优化:@runClassInSeparateProcess 的现状
PHPUnit 提供了@runClassInSeparateProcess注解,其设计意图是把整个类(包括setUpBeforeClass)放入单一子进程,理论上比@runTestsInSeparateProcesses更高效。但官方文档 attributes 部分 指出它存在 bug(Issue 5230),目前效果与@runTestsInSeparateProcesses完全相同。ItopCustomDatamodelTestCase之所以在构造函数中调用setRunClassInSeparateProcess(true),正是为了绕过这一限制(见 ItopCustomDatamodelTestCase.php)。
7.5 陷阱一:docblock 星号格式
PHPUnit 只认以/**(两个星号)开头的注释块。下面这种写法不会生效:
/* * @runTestsInSeparateProcesses必须写成:
/** * @runTestsInSeparateProcesses7.6 陷阱二:进程隔离下的 setUpBeforeClass 调用次数
setupBeforeClass的语义是"每个进程中、每个类调用一次"。因此一旦启用进程隔离,setUpBeforeClass的调用次数将与setUp一样多——每个子进程都会触发一次。编写类级初始化逻辑时(例如生成测试数据、准备临时文件)必须考虑这一特性,不能假设它只执行一次。该行为已被 experiments/runClassInSeparateProcessTest.php 用记录进程 ID(PID)的方式实证:主进程与每个子进程都会写入一次setUpBeforeClass的 PID。
8. 运行与验证
完成环境准备后,在tests/php-unit-tests目录执行:
vendor/bin/phpunit --configuration phpunit.xml.dist只跑某个测试类或方法:
vendor/bin/phpunit --configuration phpunit.xml.dist --filter UserRightsTest vendor/bin/phpunit --configuration phpunit.xml.dist --filter testReadOnlyUser需要确认测试类之间的独立性(例如重构ItopTestCase之后)时,可借助仓库自带工具 tools/run_class_by_class.php:php run_class_by_class.php会按phpunit.xml.dist声明逐个类单独执行整个套件,找出那些只在特定顺序下才通过的"隐式耦合"用例。
9. 小结
iTop 的 PHPUnit 测试体系围绕"单进程高效执行"与"防止状态泄漏"两个核心诉求设计:通过ItopTestCase → ItopDataTestCase → ItopCustomDatamodelTestCase三级基类提供能力递进的辅助设施;通过默认事务回滚、tearDown自动注销用户、DetectStaticPollutionTest静态扫描来保证环境清洁;通过精准的进程隔离注解解决少数必须隔离的用例。开发者只需遵循"尽量轻量、按需隔离、保持干净"三条原则,即可为 iTop 贡献稳定、高效、可维护的测试用例。
- 后端
- 企业应用
【免费下载链接】iTop
A simple, web based CMDB & IT Service Management tool
相关推荐
Moodle PHPUnit 测试环境搭建与单元测试编写完整指南
Moodle PHPUnit 测试环境搭建与单元测试编写完整指南 本文以 Moodle 仓库中的 public/lib/phpunit/readme.md ht
教育后端前端YOURLS 单元测试套件完全指南:本地搭建 PHPUnit 测试环境与“新进程”场景测试
YOURLS 单元测试套件完全指南:本地搭建 PHPUnit 测试环境与“新进程”场景测试 导读 本文以 YOURLS 官方测试套件( tests/README
后端Android单元测试终极指南:从隔离到集成测试的完整实践
Android单元测试终极指南:从隔离到集成测试的完整实践 Android应用开发中,单元测试是保障代码质量和稳定性的关键环节。本文将带你探索从基础隔离测试到复
文档知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考