Files
shopPRO/.paul/codebase/testing.md
2026-03-12 13:36:06 +01:00

246 lines
6.2 KiB
Markdown

# Testing Patterns
## Overview
| Metric | Value |
|--------|-------|
| Total tests | **810** |
| Total assertions | **2264** |
| 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)