246 lines
6.2 KiB
Markdown
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)
|