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

6.2 KiB

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

# 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

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)

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

$this->mockDb->method('get')->willReturn(['id' => 1, 'name' => 'Test']);

Multiple calls with different return values

$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

$this->mockDb->expects($this->once())
    ->method('delete')
    ->with('pp_shop_categories', ['id' => 5]);

Verify method never called

$this->mockDb->expects($this->never())->method('update');

Mock complex PDO statement (for ->query() calls)

$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

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}

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

Common Assertions

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