# PHP Testing Best Practices

## Standard PHP Testing Structure

### Industry Standard: PHPUnit

The de facto standard for PHP testing is **PHPUnit**. Here's how professional PHP projects handle testing:

## Proper Directory Structure

```
front-backend/
├── src/                    # Application code (or keep in root for small projects)
│   ├── Database.php
│   ├── RateLimit.php
│   └── ...
├── tests/                  # All tests go here
│   ├── Unit/              # Unit tests (test individual methods)
│   │   ├── DatabaseTest.php
│   │   └── RateLimitTest.php
│   ├── Integration/       # Integration tests (test multiple components)
│   │   └── SearchTest.php
│   └── Security/          # Security-specific tests
│       └── SecurityTest.php
├── public/                 # Public-facing files (or web root)
│   ├── index.php
│   ├── search.php
│   └── assets/
├── vendor/                 # Dependencies (git-ignored, not deployed)
├── cache/                  # Runtime cache (not deployed)
├── composer.json           # Dependency management
├── phpunit.xml            # PHPUnit configuration
└── .gitignore             # Excludes tests, vendor, cache from git
```

## What We Currently Have (Non-Standard)

```
front-backend/
├── test_search.php         ❌ Tests in root
├── test_security.php       ❌ No framework
├── test_rate_limit.php     ❌ Would deploy to production
├── create_test_db.php      ❌ Test fixture in root
└── Database.php            ✓ OK
```

## How Professional Projects Work

### 1. Composer Configuration

**composer.json** - Proper setup:
```json
{
    "require": {
        "php": "^7.4|^8.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5",
        "symfony/var-dumper": "^6.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "phpunit",
        "test:coverage": "phpunit --coverage-html coverage"
    }
}
```

**Key points:**
- `require`: Production dependencies
- `require-dev`: Development/testing dependencies (not deployed)
- `autoload-dev`: Test autoloading (not in production)
- `scripts`: Convenient test commands

### 2. PHPUnit Configuration

**phpunit.xml** - Test configuration:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
         colors="true"
         verbose="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
        <testsuite name="Security">
            <directory>tests/Security</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory suffix=".php">src</directory>
        </include>
        <exclude>
            <directory>vendor</directory>
            <directory>tests</directory>
        </exclude>
    </coverage>
</phpunit>
```

### 3. Example PHPUnit Test

**tests/Unit/DatabaseTest.php**:
```php
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use Database;

class DatabaseTest extends TestCase
{
    private $db;

    protected function setUp(): void
    {
        $this->db = Database::getInstance();
    }

    public function testGetPublishedTheses()
    {
        $results = $this->db->getPublishedTheses(10, 0);

        $this->assertIsArray($results);
        $this->assertLessThanOrEqual(10, count($results));
    }

    public function testSearchThesesWithWildcard()
    {
        $results = $this->db->searchTheses(['query' => '%'], 10, 0);

        // Should return 0 results (wildcards are escaped)
        $this->assertCount(0, $results);
    }

    public function testSearchThesesRejectsLongInput()
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Search query too long');

        $longQuery = str_repeat('a', 201);
        $this->db->searchTheses(['query' => $longQuery]);
    }

    public function testSearchThesesRejectsInvalidYear()
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Invalid year');

        $this->db->searchTheses(['year' => 999999]);
    }
}
```

### 4. Running Tests

```bash
# Install dependencies (including dev dependencies)
composer install

# Run all tests
composer test
# or
./vendor/bin/phpunit

# Run specific test suite
./vendor/bin/phpunit --testsuite Unit

# Run specific test file
./vendor/bin/phpunit tests/Unit/DatabaseTest.php

# Run with coverage report
composer test:coverage
```

### 5. .gitignore Configuration

**.gitignore**:
```
# Dependencies
/vendor/

# Test artifacts
/coverage/
/.phpunit.cache/
/phpunit.xml.local

# Cache
/cache/

# Environment
.env
.env.local

# IDE
/.idea/
/.vscode/
*.swp

# OS
.DS_Store
Thumbs.db

# Logs
*.log
error.log
```

**Important:** Tests themselves ARE committed to git, but:
- `vendor/` is excluded (regenerated via `composer install`)
- Test coverage reports are excluded
- Cache is excluded

## Production Deployment

### What Gets Deployed

```bash
# Option 1: composer install without dev dependencies
composer install --no-dev --optimize-autoloader

# This installs ONLY 'require' packages, NOT 'require-dev'
# Result: No PHPUnit, no test dependencies
```

**Deployed:**
- Application code (`src/` or root PHP files)
- Production dependencies (`vendor/` - only `require`)
- Public assets (`public/`, `assets/`)

**NOT Deployed:**
- `tests/` directory (excluded via deployment config)
- Dev dependencies (PHPUnit, etc.)
- `cache/` directory
- `.git/` directory

### Deployment Configurations

**Option 1: .deployignore** (custom deploy scripts):
```
/tests/
/coverage/
/.git/
/.github/
/cache/
phpunit.xml
phpunit.xml.dist
.env.example
README*.md
*.md
```

**Option 2: rsync with excludes** (like your justfile):
```bash
rsync -avz \
  --exclude 'tests/' \
  --exclude 'coverage/' \
  --exclude 'cache/' \
  --exclude '.git/' \
  --exclude 'phpunit.xml' \
  --exclude '*.md' \
  ./ server:/var/www/html/
```

**Option 3: Build artifact** (best for large projects):
```bash
# Build step
composer install --no-dev --optimize-autoloader
# Creates clean vendor/ with only production deps

# Then deploy only necessary files
```

## Continuous Integration (CI/CD)

Professional projects run tests automatically:

**GitHub Actions** (.github/workflows/tests.yml):
```yaml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress

      - name: Run tests
        run: composer test

      - name: Check security
        run: ./vendor/bin/phpunit --testsuite Security
```

## Test Types

### Unit Tests
Test individual methods in isolation:
```php
public function testEscapeLikeString()
{
    $db = new Database();
    $reflection = new ReflectionClass($db);
    $method = $reflection->getMethod('escapeLikeString');
    $method->setAccessible(true);

    $result = $method->invoke($db, 'test%value_here');
    $this->assertEquals('test\%value\_here', $result);
}
```

### Integration Tests
Test multiple components together:
```php
public function testSearchWithMultipleFilters()
{
    $db = Database::getInstance();
    $results = $db->searchTheses([
        'query' => 'urbain',
        'year' => 2024,
        'orientation' => 'Arts Numériques'
    ]);

    $this->assertNotEmpty($results);
    foreach ($results as $result) {
        $this->assertEquals(2024, $result['year']);
    }
}
```

### Security Tests
Test security measures:
```php
public function testSqlInjectionPrevention()
{
    $db = Database::getInstance();

    // These should not cause errors or expose data
    $malicious = ["' OR 1=1--", "'; DROP TABLE theses;--"];

    foreach ($malicious as $injection) {
        $results = $db->searchTheses(['query' => $injection]);
        // Treated as literal strings, returns valid results or empty
        $this->assertIsArray($results);
    }
}
```

## Comparison: Current vs. Standard

| Aspect | Current Approach | Standard Approach |
|--------|------------------|-------------------|
| **Location** | Root directory | `tests/` directory |
| **Framework** | Raw PHP scripts | PHPUnit |
| **Naming** | `test_*.php` | `*Test.php` |
| **Running** | `php test_file.php` | `composer test` |
| **CI/CD** | Manual | Automated |
| **Production** | Must manually exclude | Auto-excluded |
| **Coverage** | None | Built-in reporting |
| **Assertions** | Manual echoing | PHPUnit assertions |

## Migration Path for Your Project

### Minimal Changes (Keep it Simple)

If you want to keep the current simple approach but make it safer:

1. **Move tests to `tests/` directory:**
   ```bash
   mkdir tests
   mv test_*.php tests/
   mv create_test_db.php tests/fixtures/
   ```

2. **Update justfile to exclude tests:**
   ```just
   deploy:
       rsync -vur --progress \
         --exclude 'tests/' \
         --exclude 'cache/' \
         --exclude '*.db' \
         ./front-backend/ server:/var/www/html/
   ```

3. **Add .gitignore:**
   ```
   /cache/
   /vendor/
   *.log
   test.db
   ```

### Recommended Approach (Industry Standard)

For a more professional setup:

1. **Install PHPUnit:**
   ```bash
   composer require --dev phpunit/phpunit
   ```

2. **Convert tests to PHPUnit** (I can help with this)

3. **Add phpunit.xml configuration**

4. **Update deployment to use `composer install --no-dev`**

## Benefits of Standard Approach

1. **Automatic Exclusion**: Tests never deployed by accident
2. **Better Assertions**: PHPUnit provides rich assertion library
3. **Coverage Reports**: See which code is tested
4. **CI/CD Integration**: Automated testing on every commit
5. **IDE Support**: Better integration with PHPStorm, VSCode
6. **Mocking**: Easy to mock dependencies
7. **Data Providers**: Test same logic with multiple inputs
8. **Professional**: Expected by other developers

## Quick Decision Guide

**Keep Simple Approach If:**
- ✓ Small project (< 10 files)
- ✓ Solo developer
- ✓ No CI/CD pipeline
- ✓ You manually test before deploy

**Use PHPUnit If:**
- ✓ Team project
- ✓ Growing codebase
- ✓ Want automated testing
- ✓ Need coverage reports
- ✓ Planning CI/CD

## Recommendation for Your Project

Given your project size, I'd suggest a **hybrid approach**:

1. **Move tests to `tests/` directory** (immediate)
2. **Update deployment to exclude `tests/`** (immediate)
3. **Keep simple PHP test scripts for now** (works fine)
4. **Migrate to PHPUnit later** (when project grows)

Would you like me to help with any of these approaches?
