# 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)