UPDATE
This commit is contained in:
245
.paul/codebase/testing.md
Normal file
245
.paul/codebase/testing.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user