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 --report and --auto-skip for 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

  1. Run tests before committing: Ensure all tests pass locally
  2. Add tests for new features: Cover new functionality with tests
  3. Add tests for bug fixes: Prevent regression with test cases
  4. Keep tests maintainable: Clear, simple tests are easier to maintain
  5. Test edge cases: Consider boundary conditions and error cases
  6. Use meaningful assertions: Choose assertions that clearly express intent
  7. 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-report is 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:

  1. Check this documentation
  2. Review existing tests for examples
  3. Open an issue on GitHub with details about the test failure