mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 15:34:12 +01:00
250 lines
8.9 KiB
Markdown
250 lines
8.9 KiB
Markdown
# PHP unitary tests
|
||
|
||
Documentation on creating and maintaining tests in iTop.
|
||
|
||
## Prerequisites
|
||
|
||
### PHPUnit configuration file
|
||
A default file is located in `/tests/php-unit-tests/phpunit.xml.dist`
|
||
|
||
If you need to customize it, copy it to `phpunit.xml` (not versioned).
|
||
|
||
### PHP configuration
|
||
* PHPUnit configuration file
|
||
- `memory_limit`: as the tests are for the most part ran in the same process, memory usage may become an issue! A default value is set in default PHPUnit configuration XML file, don't hesitate to update it if needed
|
||
* PHP CLI php.ini
|
||
- enable OpCache
|
||
- disable Xdebug (xdebug.mode=off) : huge performance improvements (between X2 and X3), and we can still debug using PHPStorm !
|
||
|
||
### Dependencies
|
||
Whereas iTop dependencies are bundled inside its repository, the tests dependencies are not, and must be added manually. To do so, run `composer install` in the `/tests/php-unit-tests` directory.
|
||
|
||
### iTop instance prerequisites to run its test suite
|
||
Install iTop with default setup options :
|
||
- Configuration Management options : everything checked
|
||
- Service Management for Enterprises
|
||
- Simple Ticket Management + Customer portal
|
||
- Simple Change Management
|
||
|
||
Plus :
|
||
- Additional ITIL tickets : check Known Errors Management and FAQ
|
||
|
||
## Create an iTop PHPUnit test
|
||
|
||
### Where should I add my test?
|
||
- Covers an iTop PHP class or method? => Most likely in the `unitary-tests` directory
|
||
- Covers the consistency of some data through the app? => Most likely in `integration-tests` directory
|
||
|
||
### iTop test parent classes
|
||
iTop provides PHPUnit TestCase children that provides some helpers and setUp/tearDown overrides :
|
||
- `\Combodo\iTop\Test\UnitTest\ItopTestCase` : for the most simple iTop tests
|
||
- `\Combodo\iTop\Test\UnitTest\ItopDataTestCase` : to get a started metamodel and have cleanup of CRUD operations on iTop objects (transactions by default)
|
||
- `\Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase` : to test a non standard datamodel (available since iTop 2.7.9, 3.0.4, 3.1.0 N°6097)
|
||
|
||
|
||
### Naming convention
|
||
* to test `MyClass` class then create a `MyClassTest` class
|
||
* to test `MyMethod` method, the corresponding test method should be named `testMyMethod`
|
||
|
||
Source [PHPUnit Manual – Chapter 2. Writing Tests for PHPUnit](https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#writing-tests-for-phpunit)
|
||
|
||
### What about skipped tests ?
|
||
A test can be marked as skipped by using the `markTestAsSkipped()` PHPUnit method. Please use it only for temporary disabled tests, for example the ones that are pushed before their corresponding fix.
|
||
|
||
For other cases like non-relevant data provider cases, just mark the test valid with `assertTrue(true)` and `return`.
|
||
|
||
|
||
|
||
|
||
## Tips: generic PHPUnit
|
||
|
||
### Disable a test
|
||
```php
|
||
$this->markTestSkipped('explanation');
|
||
```
|
||
|
||
### Test an exception
|
||
Just before calling the code throwing the exception, call `\PHPUnit\Framework\TestCase::expectException`. You might also use `expectExceptionMessage` and/or `expectExceptionMessageMatches`.
|
||
|
||
Example :
|
||
|
||
```php
|
||
// Try to delete the tag, must complain !
|
||
$this->expectException(DeleteException::class);
|
||
$this->expectExceptionMatches('/'.$this->GetKey().'/');
|
||
$oTagData->DBDelete();
|
||
```
|
||
|
||
> [!WARNING]
|
||
> When the condition is met the test is finished and following code will be ignored !
|
||
|
||
Another way to do is using try/catch blocks, for example :
|
||
```php
|
||
$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; // handles the fail() call just above
|
||
} 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()
|
||
);
|
||
}
|
||
```
|
||
|
||
|
||
|
||
|
||
|
||
## Tips: iTop tests
|
||
|
||
### Load an iTop class which is outside the autoloader
|
||
When running a test extending ItopDataTestCase you'll get all of the iTop classes loaded (Composer autoloader + iTop autoloader including installed modules)
|
||
|
||
For ItopTestCase files, you may need to load specific iTop classes that aren't part of the Composer autoloader. If so, since N°5608 (introduced in iTop 2.7.9, 3.0.3, 3.1.0-1) you can use :
|
||
- ItopTestCase::RequireOnceItopFile
|
||
- ItopTestCase::RequireOnceUnitTestFile
|
||
|
||
### Add a User context
|
||
Use `UserRights::Login()`
|
||
|
||
|
||
|
||
|
||
## Test performances
|
||
|
||
### Memory limit
|
||
|
||
As the tests are run in the same process, memory usage
|
||
may become an issue as soon as tests are all executed at once.
|
||
|
||
Fix that in the XML configuration in the PHP section
|
||
```xml
|
||
<ini name="memory_limit" value="512M"/>
|
||
```
|
||
|
||
|
||
### Measure the time spent in a test
|
||
|
||
Simply cut'n paste the following line at several places within the test function:
|
||
|
||
```php
|
||
if (isset($fStarted)) {echo 'L'.__LINE__.': '.round(microtime(true) - $fStarted, 3)."\n";} $fStarted = microtime(true);
|
||
```
|
||
|
||
### Derive from the relevant test class
|
||
|
||
Whenever possible keep it the most simple, hence you should first
|
||
attempt to derive from `TestCase`.
|
||
|
||
Then, you might need to derive from `ItopTestCase`.
|
||
|
||
Finally, as a last resort, you will use `ItopDataTestCase`.
|
||
|
||
### Determine the most relevant isolation configuration
|
||
|
||
Should you have opted for `ItopDataTestCase`, then you will have to follow these steps:
|
||
|
||
1) Build you test class until it is successfull, without process isolation.
|
||
2) Run the whole test suite [unitary-tests](unitary-tests)
|
||
3) If a false-positive appears, then you will start troubleshooting. One advise: be positive!
|
||
|
||
### Leave the place clean
|
||
|
||
To check your code against polluting coding patterns, run the test [integration-tests/DetectStaticPollutionTest.php](integration-tests/DetectStaticPollutionTest.php)
|
||
It will tell you if something is wrong, either in your code, or anywhere else in the tests.
|
||
Fortunately, it will give you an alternative.
|
||
|
||
Detected patterns:
|
||
* ContextTag::addTag()
|
||
* EventService::RegisterListener()
|
||
* Dict::Add()
|
||
|
||
|
||
By the way, some patterns do not pollute, because they are handled by the test framework:
|
||
* Configuration : automatically reset after test class execution
|
||
* UserRights : a logoff is performed after each test execution
|
||
* Dict::SetUserLanguage: the user language is reset after each test execution
|
||
|
||
See also `@beforeClass` and `@afterClass` to handle cleanup.
|
||
|
||
If you can't, then ok you will have to isolate it!
|
||
|
||
|
||
|
||
|
||
## PHPUnit process isolation
|
||
|
||
### Understand tests interactions
|
||
|
||
With PHPStorm, select two tests, right click to get the context menu, then `run`.
|
||
|
||
You will have both tests executed and you will be able to figure out if the first one has an impact on the second one.
|
||
|
||
### About process isolation
|
||
#### Isolation with PHPUnit
|
||
|
||
By default, tests are run in a single process launched by PHPUnit.
|
||
|
||
If process isolation is configured for some tests, then those tests
|
||
will be executed in a separate process. The main process will
|
||
continue executing non isolated tests.
|
||
|
||
#### Cost of isolation
|
||
|
||
The cost of isolating a very basic `TestCase` is approximately 4 ms.
|
||
|
||
The cost of isolating an `ItopDataTestCase` is approximately 800 ms.
|
||
|
||
### Isolation within iTop
|
||
|
||
#### At the test level (preferred)
|
||
Add annotation `@runInSeparateProcess`
|
||
Each and every test case will run in a separate
|
||
process.
|
||
|
||
Note : before N°6658 (3.0.4 / 3.1.1 / 3.2.0) we were also adding the `@backupGlobals disabled`
|
||
and `@preserveGlobalState disabled` annotations. This is no longer necessary as the first has this default value
|
||
already, and the second one is now set in iTopTestCase as a PHP class attribute.
|
||
|
||
#### At the test class level
|
||
Add annotation `@runTestsInSeparateProcesses`
|
||
Each and every test case in the class will run in a separate
|
||
process.
|
||
|
||
#### Globally (never do that)
|
||
Set it into [phpunit.xml.dist](phpunit.xml.dist)
|
||
|
||
### Further enhancements
|
||
The annotation [`@runClassInSeparateProcess`](https://docs.phpunit.de/en/10.0/attributes.html?highlight=runclassinseparateprocess#runclassinseparateprocess) is supposed to do the perfect job, but it is buggy [(See Issue 5230)](https://github.com/sebastianbergmann/phpunit/issues/5230) and it has
|
||
the exact same effect as `@runTestsInSeparateProcesses`.
|
||
|
||
Note : this option is documented only in the [attributes part of the documentation](https://docs.phpunit.de/en/10.0/attributes.html).
|
||
|
||
### Traps
|
||
#### Doc block comment format : when it is a matter of stars
|
||
```php
|
||
/*
|
||
* @runTestsInSeparateProcesses
|
||
```
|
||
This won't work because the comment MUST start with `/**` (two stars) to be considerer by PHPUnit.
|
||
|
||
#### SetupBeforeClass called more often than expected when running in separate processes
|
||
|
||
`setupBeforeClass` is called once for the class **in a given process**.
|
||
|
||
Therefore, if the tests are isolated, then `setupBeforeClass` will be called as often as `setUp`.
|
||
|