# Testing Patterns

## Overview

| Metric | Value |
|--------|-------|
| Total tests | **841** |
| Total assertions | **2330** |
| Framework | PHPUnit 9.6 (`phpunit.phar`) |
| Bootstrap | `tests/bootstrap.php` |
| Config | `phpunit.xml` |

## Running Tests

```bash
# Full suite (PowerShell â€” recommended)
./test.ps1

# Specific file
./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php

# Specific test method
./test.ps1 --filter testGetQuantityReturnsCorrectValue

# Alternatives
composer test        # standard output
./test.bat           # testdox (readable list)
./test-simple.bat    # dots
./test-debug.bat     # debug output
./test.sh            # Git Bash
```

## Test Structure

Tests mirror source structure:

```
tests/Unit/
â”śâ”€â”€ Domain/
â”‚   â”śâ”€â”€ Product/ProductRepositoryTest.php
â”‚   â”śâ”€â”€ Category/CategoryRepositoryTest.php
â”‚   â”śâ”€â”€ Order/OrderRepositoryTest.php
â”‚   â””â”€â”€ ... (all 29 modules covered)
â”śâ”€â”€ admin/Controllers/
â”‚   â”śâ”€â”€ ShopCategoryControllerTest.php
â”‚   â””â”€â”€ ...
â””â”€â”€ api/
    â””â”€â”€ ...
```

## Test Class Pattern

```php
namespace Tests\Unit\Domain\Category;

use PHPUnit\Framework\TestCase;
use Domain\Category\CategoryRepository;

class CategoryRepositoryTest extends TestCase
{
    private $mockDb;
    private CategoryRepository $repository;

    protected function setUp(): void
    {
        $this->mockDb = $this->createMock(\medoo::class);
        $this->repository = new CategoryRepository($this->mockDb);
    }

    // Tests follow below...
}
```

## AAA Pattern (Arrange-Act-Assert)

```php
public function testGetQuantityReturnsCorrectValue(): void
{
    // Arrange
    $this->mockDb->expects($this->once())
        ->method('get')
        ->with(
            'pp_shop_products',
            'quantity',
            ['id' => 123]
        )
        ->willReturn(42);

    // Act
    $result = $this->repository->getQuantity(123);

    // Assert
    $this->assertSame(42, $result);
}
```

## Mock Patterns

### Simple return value
```php
$this->mockDb->method('get')->willReturn(['id' => 1, 'name' => 'Test']);
```

### Multiple calls with different return values
```php
$this->mockDb->method('get')
    ->willReturnCallback(function ($table, $columns, $where) {
        if ($table === 'pp_shop_categories') {
            return ['id' => 15, 'status' => '1'];
        }
        return null;
    });
```

### Verify exact call arguments
```php
$this->mockDb->expects($this->once())
    ->method('delete')
    ->with('pp_shop_categories', ['id' => 5]);
```

### Verify method never called
```php
$this->mockDb->expects($this->never())->method('update');
```

### Mock complex PDO statement (for `->query()` calls)
```php
$countStmt = $this->createMock(\PDOStatement::class);
$countStmt->method('fetchAll')->willReturn([[25]]);

$productsStmt = $this->createMock(\PDOStatement::class);
$productsStmt->method('fetchAll')->willReturn([['id' => 301], ['id' => 302]]);

$callIndex = 0;
$this->mockDb->method('query')
    ->willReturnCallback(function () use (&$callIndex, $countStmt, $productsStmt) {
        $callIndex++;
        return $callIndex === 1 ? $countStmt : $productsStmt;
    });
```

## Controller Test Pattern

```php
class ShopCategoryControllerTest extends TestCase
{
    protected function setUp(): void
    {
        $this->repository = $this->createMock(CategoryRepository::class);
        $this->languagesRepository = $this->createMock(LanguagesRepository::class);
        $this->controller = new ShopCategoryController(
            $this->repository,
            $this->languagesRepository
        );
    }

    // Verify constructor signature
    public function testConstructorRequiresCorrectRepositories(): void
    {
        $reflection = new \ReflectionClass(ShopCategoryController::class);
        $params = $reflection->getConstructor()->getParameters();

        $this->assertCount(2, $params);
        $this->assertEquals(
            'Domain\\Category\\CategoryRepository',
            $params[0]->getType()->getName()
        );
    }

    // Verify action methods return string
    public function testViewListReturnsString(): void
    {
        $this->repository->method('categoriesList')->willReturn([]);
        $result = $this->controller->view_list();
        $this->assertIsString($result);
    }

    // Verify expected methods exist
    public function testHasExpectedActionMethods(): void
    {
        $this->assertTrue(method_exists($this->controller, 'view_list'));
        $this->assertTrue(method_exists($this->controller, 'category_edit'));
    }
}
```

## Test Naming Convention

Pattern: `test{What}{WhenCondition}`

```php
testGetQuantityReturnsCorrectValue()
testGetQuantityReturnsNullWhenProductNotFound()
testCategoryDetailsReturnsDefaultForInvalidId()
testCategoryDeleteReturnsFalseWhenHasChildren()
testCategoryDeleteReturnsTrueWhenDeleted()
testSaveCategoriesOrderReturnsFalseForNonArray()
testPaginatedCategoryProductsClampsPage()
```

## Common Assertions

```php
$this->assertTrue($bool);
$this->assertFalse($bool);
$this->assertEquals($expected, $actual);
$this->assertSame($expected, $actual);        // type-strict
$this->assertNull($value);
$this->assertIsArray($value);
$this->assertIsInt($value);
$this->assertIsString($value);
$this->assertEmpty($array);
$this->assertCount(3, $array);
$this->assertArrayHasKey('id', $array);
$this->assertArrayNotHasKey('foo', $array);
$this->assertGreaterThanOrEqual(3, $count);
$this->assertInstanceOf(ClassName::class, $obj);
```

## Available Stubs (`tests/stubs/`)

| Stub | Purpose |
|------|---------|
| `Helpers.php` | `Helpers::seo()`, `::lang()`, `::send_email()`, `::normalize_decimal()` |
| `ShopProduct.php` | Legacy `shop\Product` class stub |
| `RedisConnection` | Redis singleton stub (auto-loaded from bootstrap) |
| `CacheHandler` | Cache stub (no actual Redis needed in tests) |

## What's Covered

- All 29 Domain repositories âś“
- Core business logic (quantity, pricing, category tree) âś“
- Query behavior with mocked Medoo âś“
- Cache patterns âś“
- Controller constructor injection âś“
- `FormValidator` behavior âś“
- API controllers âś“

## What's Lightly Covered

- Full controller action execution (template rendering)
- Session state in tests
- AJAX response integration
- Frontend Views (static classes)

