Testing Guide
October 8, 2025 · View on GitHub
This document provides information on how to run and write tests for the PHP Antimalware Scanner project.
Overview
The test suite consists of:
- Unit Tests: Fast, isolated tests for individual classes and methods
- Integration Tests: Tests that execute the scanner CLI with various configurations
Running Tests
Prerequisites
Install dependencies including PHPUnit:
composer install
Run All Tests
composer test
# or
phpunit
Run Specific Test Suites
Unit tests only:
composer test:unit
# or
phpunit --testsuite unit
Integration tests only:
composer test:integration
# or
phpunit --testsuite integration
Run Specific Test Files
phpunit tests/Unit/PathTest.php
phpunit tests/Integration/DefaultScanTest.php
Run Specific Test Methods
phpunit --filter testScanCleanDirectoryExitsWithZero
Code Coverage
Generate HTML coverage report:
composer test:coverage
Coverage report will be generated in the coverage/ directory.
Test Structure
tests/
├── Unit/ # Unit tests for individual classes
│ ├── PathTest.php # Tests for Path helper
│ ├── CodeMatchTest.php # Tests for CodeMatch utility
│ └── DeobfuscatorTest.php # Tests for Deobfuscator
├── Integration/ # CLI integration tests
│ ├── CLITestCase.php # Base class for CLI tests
│ ├── DefaultScanTest.php # Default scanning behavior
│ ├── ReportModeTest.php # Report generation tests
│ ├── ScanModesTest.php # Predefined mode tests
│ └── PathControlsTest.php # Path filtering tests
└── Fixtures/ # Test data files
├── clean/ # Clean PHP files
├── malware/ # Malware samples
├── obfuscated/ # Obfuscated code samples
├── reports/ # Generated reports (temp)
└── tmp/ # Temporary test files (temp)
Writing Tests
Unit Tests
Unit tests should extend PHPUnit\Framework\TestCase and test individual methods in isolation:
<?php
namespace AMWScan\Tests\Unit;
use AMWScan\Path;
use PHPUnit\Framework\TestCase;
class PathTest extends TestCase
{
public function testGetNormalizesPath()
{
$path = Path::get('/path/to//file.php');
$this->assertEquals('/path/to/file.php', $path);
}
}
Guidelines:
- Test one thing per test method
- Use descriptive test names (
testMethodNameBehavior) - Keep tests fast and isolated
- Avoid external dependencies (filesystem, network)
- Clean up any resources in
tearDown()
Integration Tests
Integration tests should extend AMWScan\Tests\Integration\CLITestCase and test the scanner via CLI:
<?php
namespace AMWScan\Tests\Integration;
class MyIntegrationTest extends CLITestCase
{
public function testScannerDetectsMalware()
{
$result = $this->runScanner(
$this->fixturesPath . '/malware',
['--report', '--auto-skip']
);
$this->assertEquals(1, $result['exitCode']);
$this->assertStringContainsString('detected', strtolower($result['output']));
}
}
Available helper methods:
$this->runScanner($path, $args)- Execute scanner with arguments$this->fixturesPath- Path to test fixtures$this->reportsPath- Path for test reports$this->tmpPath- Path for temporary files$this->cleanupTestArtifacts()- Clean up test files
Guidelines:
- Use
--reportand--auto-skipfor non-interactive tests - Always clean up generated files
- Test exit codes and output messages
- Use explicit report paths when testing report generation
- Keep tests independent (don't rely on execution order)
Adding Test Fixtures
Clean Files
Add benign PHP files to tests/Fixtures/clean/:
<?php
// Clean PHP file example
function safeFunction($input) {
return htmlspecialchars($input);
}
Malware Samples
Add malware patterns to tests/Fixtures/malware/:
<?php
// Example malware pattern
eval($_POST['cmd']);
Important: These are test samples only. They should not be executable or contain real malicious payloads.
Obfuscated Code
Add obfuscated code to tests/Fixtures/obfuscated/:
<?php
// Example obfuscation
$x = chr(101).chr(118).chr(97).chr(108);
Continuous Integration
Tests run automatically on:
- Pull requests to main/master branches
- Pushes to main/master branches
- Multiple PHP versions (7.4, 8.0, 8.1, 8.2, 8.3)
See .github/workflows/php.yml for CI configuration.
Debugging Tests
Verbose Output
phpunit --verbose
Stop on First Failure
phpunit --stop-on-failure
Debug Specific Test
phpunit --filter testName --debug
View Scanner Output
Integration tests capture scanner output. To see it:
phpunit --verbose
Or add debug output in tests:
$result = $this->runScanner(...);
echo $result['output'];
Best Practices
- Run tests before committing: Ensure all tests pass locally
- Add tests for new features: Cover new functionality with tests
- Add tests for bug fixes: Prevent regression with test cases
- Keep tests maintainable: Clear, simple tests are easier to maintain
- Test edge cases: Consider boundary conditions and error cases
- Use meaningful assertions: Choose assertions that clearly express intent
- Avoid test interdependencies: Each test should be runnable in isolation
Common Issues
Tests Fail Locally But Pass in CI (or vice versa)
- Check PHP version differences
- Verify dependencies are up to date (
composer update) - Check for filesystem path differences (Windows vs Unix)
Integration Tests Timeout
- Increase timeout in phpunit.xml
- Check if scanner enters interactive mode (use
--report --auto-skip) - Verify fixtures are not too large
Report Files Not Found
- Ensure explicit
--path-reportis provided - Check cleanup code doesn't remove files prematurely
- Verify report generation is enabled (not
--disable-report)
Resources
Getting Help
If you encounter issues with tests:
- Check this documentation
- Review existing tests for examples
- Open an issue on GitHub with details about the test failure