# Test Writing Guide

This guide defines standards for writing maintainable, meaningful tests using PHPUnit for Duplicator Pro.

## Purpose and Principles

Tests should:
- **Verify real behavior** - Test scenarios that can actually happen, not theoretical edge cases
- **Be precise and specific** - Assert exact values, not approximations
- **Avoid obvious redundancies** - Don't test WordPress core, third-party libraries, or framework code
- **Avoid code duplication** - Use helper functions and extract common patterns

## Test Organization

```
tests/
├── Unit/              # Isolated logic tests
├── Feature/           # Integration tests
├── Concerns/          # Reusable test traits
└── TestsUtils/        # Test utilities and helpers
```

**Unit tests:** Test individual methods in isolation. Fast, focused, no external dependencies.

**Feature tests:** Test complete workflows. May interact with WordPress functions, database, or filesystem.

## Writing Good Tests

**Test real scenarios:**
- Edge cases that can actually happen
- Business logic and domain rules
- WordPress integration points
- File and database operations
- User permissions and capabilities

**Be precise:**
- Use `assertCount(2, $items)` not `assertTrue(count($items) > 1)`
- Use `assertSame('expected', $actual)` not `assertTrue($actual == 'expected')`
- Use specific assertions: `assertInstanceOf`, `assertArrayHasKey`, `assertStringContainsString`

**Avoid code duplication:**
- Use available test helpers for common operations
- Extract repeated code into helper methods or traits
- Create test utilities in `tests/TestsUtils/`

**Focus on your logic:**
- Test your implemented business logic, not WordPress/library internals
- Test through public interfaces, not private methods
- Avoid testing constants, enum values, or standard framework behavior

**Be pragmatic:**
- Focus on realistic scenarios that can actually occur
- Avoid edge cases that are prevented by type systems or validation
- If a scenario requires breaking PHP type safety or WordPress constraints, it's not worth testing

### Test Naming

**Pattern:** `test_{what}_{expected_outcome}`

Examples:
- `test_package_creation_succeeds_with_valid_data()`
- `test_unauthorized_user_cannot_create_package()`
- `test_archive_creation_handles_large_files()`
- `test_storage_upload_retries_on_failure()`

## PHPUnit Commands

Run tests using Composer scripts:

```bash
# Install PHPUnit dependencies
composer phpunit-install

# Run all tests (latest PHP)
composer phpunit

# Run tests with PHP 7.4 compatibility
composer phpunit-74
```

## WordPress Testing Environment

Tests use WordPress test framework:
- Bootstrap file: `tests/bootstrap.php`
- Tests discover files ending in `Tests.php`
- WordPress functions available in test environment
- Database operations use WordPress `wpdb`

## Test Structure

```php
<?php

namespace Duplicator\Tests\Unit\Package;

use Duplicator\Package\DupPackage;
use PHPUnit\Framework\TestCase;

class PackageCreationTests extends TestCase
{
    public function test_package_creation_succeeds_with_valid_data()
    {
        // Arrange
        $config = ['name' => 'Test Package'];

        // Act
        $package = DupPackage::create($config);

        // Assert
        $this->assertInstanceOf(DupPackage::class, $package);
        $this->assertSame('Test Package', $package->getName());
    }
}
```

## Common Assertions

**Identity and equality:**
```php
$this->assertSame($expected, $actual);      // Strict comparison (===)
$this->assertEquals($expected, $actual);     // Loose comparison (==)
$this->assertTrue($condition);
$this->assertFalse($condition);
$this->assertNull($value);
```

**Types and instances:**
```php
$this->assertInstanceOf(ClassName::class, $object);
$this->assertIsString($value);
$this->assertIsArray($value);
$this->assertIsInt($value);
```

**Arrays and collections:**
```php
$this->assertCount(3, $array);
$this->assertEmpty($array);
$this->assertArrayHasKey('key', $array);
$this->assertContains('value', $array);
```

**Strings:**
```php
$this->assertStringContainsString('needle', $haystack);
$this->assertStringStartsWith('prefix', $string);
$this->assertStringEndsWith('suffix', $string);
$this->assertMatchesRegularExpression('/pattern/', $string);
```

**Files:**
```php
$this->assertFileExists($path);
$this->assertFileIsReadable($path);
$this->assertDirectoryExists($path);
```

## Testing WordPress Integration

**Testing capabilities:**
```php
$user = $this->factory()->user->create(['role' => 'administrator']);
wp_set_current_user($user);

$this->assertTrue(current_user_can('export'));
```

**Testing hooks:**
```php
$callback_fired = false;
add_action('duplicator_test_hook', function() use (&$callback_fired) {
    $callback_fired = true;
});

do_action('duplicator_test_hook');
$this->assertTrue($callback_fired);
```

**Testing database operations:**
```php
global $wpdb;

$result = $wpdb->insert($wpdb->prefix . 'duplicator_packages', [
    'name' => 'Test Package',
    'hash' => 'abc123'
]);

$this->assertSame(1, $result);
$this->assertSame(1, $wpdb->rows_affected);
```

## Testing File Operations

**Testing archive creation:**
```php
$archive_path = '/tmp/test-archive.zip';

// Create archive
$result = $archiver->create($archive_path, $files);

// Assert
$this->assertFileExists($archive_path);
$this->assertGreaterThan(0, filesize($archive_path));

// Cleanup
unlink($archive_path);
```

**Testing file scanning:**
```php
$scanner = new FileScanner('/path/to/scan');
$files = $scanner->scan();

$this->assertIsArray($files);
$this->assertCount(10, $files);
$this->assertArrayHasKey('size', $files[0]);
```

## Mocking and Stubs

Use PHPUnit mocking when necessary:

```php
$storage_mock = $this->createMock(AbstractStorageEntity::class);
$storage_mock->method('upload')
    ->willReturn(true);

$result = $package->uploadToStorage($storage_mock);
$this->assertTrue($result);
```

## Testing Exceptions

```php
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid package name');

$package = DupPackage::create(['name' => '']);
```

## Test Data and Fixtures

Create test data in `tests/TestsUtils/`:

```php
class TestDataFactory
{
    public static function createPackage(array $overrides = []): DupPackage
    {
        $defaults = [
            'name' => 'Test Package',
            'hash' => 'abc123',
            // ... other defaults
        ];

        return DupPackage::create(array_merge($defaults, $overrides));
    }
}
```

## Best Practices

**Do:**
- Test one concept per test method
- Use descriptive test names
- Arrange-Act-Assert structure
- Clean up resources (files, database entries)
- Test both success and failure cases
- Test edge cases and boundary conditions

**Don't:**
- Test private methods directly
- Test WordPress core functionality
- Test third-party library internals
- Create overly complex test setups
- Use sleep() or time-dependent assertions
- Leave test data in filesystem or database

## Related Documentation

- **[PHPUnit Documentation](https://docs.phpunit.de/)** - Official PHPUnit guide
- **[WordPress Testing](https://make.wordpress.org/core/handbook/testing/automated-testing/phpunit/)** - WordPress PHPUnit handbook
- **[PHPUnit Assertions](https://docs.phpunit.de/en/11.5/assertions.html)** - Complete assertion reference
