Testing & Code Quality

25 minutes INTERMEDIATE

Master testing and code quality in GEMVC. Learn PHPStan Level 9 (the highest static analysis level), PHPUnit, and Pest testing frameworks. GEMVC is one of the few frameworks that supports PHPStan Level 9!

What You'll Learn

PHPStan Level 9

Static analysis and type safety

PHPUnit Testing

Unit and integration testing

Pest Testing

Modern testing framework alternative

Video Coming Soon...

Why GEMVC is Special for Testing

GEMVC is one of the few existing frameworks that supports PHPStan Level 9 - the highest level of static analysis in PHP! This means you can write type-safe, bug-free code with the best IDE support and catch errors before they reach production.

๐ŸŽฏ Why Other Frameworks Can't Achieve Level 9:

  • โ€ข Laravel/Symfony: Magic methods and dynamic facades break static analysis
  • โ€ข Eloquent/Doctrine: Complex ORMs make type inference impossible
  • โ€ข Dynamic Properties: Framework magic defeats static analysis

GEMVC's Advantage: Clean, explicit, type-safe design from the ground up!

  • PHPStan Level 9 - Static analysis catches bugs before runtime
  • PHPUnit - Industry-standard unit testing framework
  • Pest - Modern, elegant testing framework alternative
  • Automatic Configuration - All tools configured during gemvc init
  • Pre-configured - Ready to use immediately after installation
1

Installation During gemvc init

Choose Your Testing Tools

During gemvc init, you'll be prompted to install testing and code quality tools:

Terminal
# Initialize project
gemvc init

# When prompted:
# Install PHPStan? (y/n): y  โ† STRONGLY RECOMMENDED!
# Install testing framework? (y/n): y
# Choose testing framework:
#   1) PHPUnit
#   2) Pest
# Select: 1 or 2

โš ๏ธ Important:

PHPStan is strongly recommended! It will be automatically installed and configured in your project. Use it during development to catch type errors before they reach production.

What Gets Installed:

  • โœ“ PHPStan Level 9 (if selected) - Pre-configured phpstan.neon
  • โœ“ PHPUnit or Pest (if selected) - Test framework with configuration
  • โœ“ OpenSwoole stubs - For proper type checking with OpenSwoole
  • โœ“ Redis stubs - For connection type safety
  • โœ“ Test directory structure - Ready for your tests
2

PHPStan Level 9 - Static Analysis

What is PHPStan Level 9?

PHPStan Level 9 is the highest level of static analysis in PHP. It performs the most thorough type checking possible, catching bugs before runtime.

PHPStan Level 9 Catches:

  • โœ“ Type errors before runtime
  • โœ“ Null pointer exceptions
  • โœ“ Undefined method calls
  • โœ“ Incorrect array access
  • โœ“ Type mismatches
  • โœ“ Missing return types
  • โœ“ Incorrect property access
  • โœ“ And much more!

Running PHPStan

After installation, run PHPStan to check your code:

Terminal
# Run PHPStan analysis
vendor/bin/phpstan analyse

# Or use composer script (if configured)
composer phpstan

# With baseline (ignore existing errors)
vendor/bin/phpstan analyse --generate-baseline

โŒ Without PHPStan (Bugs in Production):

Buggy Code
<?php
public function getUser($id)
{
    // โŒ No type hints
    // โŒ Might return null - will cause error!
    return $this->selectById($id)->name;
}

// Runtime error: Trying to get property 'name' of null

โœ… With PHPStan Level 9 (Caught at Development):

Type-Safe Code
<?php
public function getUser(int $id): ?UserModel
{
    // โœ… Type hints
    $user = $this->selectById($id);
    
    if (!$user) {
        return null;  // โœ… Handle null case
    }
    
    return $user;  // โœ… Type-safe!
}

// PHPStan ensures this is always safe

Benefits of PHPStan Level 9:

  • โœ“ Type Safety - Catch errors before they happen
  • โœ“ Better IDE Support - Full autocomplete, refactoring, navigation
  • โœ“ Cleaner Code - Forces explicit, readable types
  • โœ“ Fewer Bugs - Static analysis catches issues early
  • โœ“ Team Consistency - Everyone writes code the same way
  • โœ“ Self-Documenting - Types communicate intent clearly
3

PHPStan Configuration

Automatic Configuration

When you install PHPStan during gemvc init, it automatically creates a phpstan.neon configuration file:

phpstan.neon - Auto-Generated Configuration
parameters:
    level: 9  # Highest level!
    paths:
        - app
    excludePaths:
        - app/vendor
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: true
    checkUninitializedProperties: true
    checkNullables: true
    checkThisOnly: false
    checkUnionTypes: true
    checkExplicitMixedMissingReturn: true

What's Included:

  • โœ“ Level 9 - Highest static analysis level
  • โœ“ OpenSwoole Stubs - Proper type checking for OpenSwoole
  • โœ“ Redis Stubs - Connection type safety
  • โœ“ App Directory - Analyzes your application code
  • โœ“ All Checks Enabled - Maximum type safety
4

PHPUnit Testing

Industry-Standard Testing

PHPUnit is the most popular testing framework for PHP. GEMVC automatically configures it when you select it during gemvc init.

Running PHPUnit Tests

Terminal
# Run all tests
vendor/bin/phpunit

# Run specific test file
vendor/bin/phpunit tests/UserTest.php

# Run with coverage
vendor/bin/phpunit --coverage-html coverage/

# Or use composer script
composer test
tests/UserModelTest.php - Example Test
<?php
namespace Tests;

use PHPUnit\Framework\TestCase;
use App\Model\UserModel;
use App\Table\UserTable;

class UserModelTest extends TestCase
{
    public function testCreateUser(): void
    {
        $user = new UserModel();
        $user->name = 'John Doe';
        $user->email = 'john@example.com';
        $user->setPassword('password123');
        
        $result = $user->createModel();
        
        $this->assertEquals(201, $result->response_code);
        $this->assertEquals('created', $result->message);
        $this->assertNotNull($user->id);
    }
    
    public function testSelectByEmail(): void
    {
        $userTable = new UserTable();
        $user = $userTable->selectByEmail('john@example.com');
        
        $this->assertInstanceOf(UserModel::class, $user);
        $this->assertEquals('john@example.com', $user->email);
    }
    
    public function testDuplicateEmail(): void
    {
        $user1 = new UserModel();
        $user1->name = 'User 1';
        $user1->email = 'duplicate@example.com';
        $user1->setPassword('pass123');
        $user1->createModel();
        
        $user2 = new UserModel();
        $user2->name = 'User 2';
        $user2->email = 'duplicate@example.com';
        $user2->setPassword('pass123');
        $result = $user2->createModel();
        
        $this->assertEquals(422, $result->response_code);
        $this->assertStringContainsString('already exists', $result->service_message);
    }
}

PHPUnit Features:

  • โœ“ Unit Tests - Test individual methods and classes
  • โœ“ Integration Tests - Test complete workflows
  • โœ“ Assertions - Comprehensive assertion library
  • โœ“ Test Coverage - Measure code coverage
  • โœ“ Mocking - Mock dependencies for isolated testing
5

Pest Testing

Modern, Elegant Testing

Pest is a modern testing framework built on top of PHPUnit. It provides a more elegant, readable syntax while maintaining all PHPUnit features.

Running Pest Tests

Terminal
# Run all tests
vendor/bin/pest

# Run specific test file
vendor/bin/pest tests/UserTest.php

# Run with coverage
vendor/bin/pest --coverage

# Or use composer script
composer test
tests/UserTest.php - Pest Example
<?php
use App\Model\UserModel;
use App\Table\UserTable;

test('creates user successfully', function () {
    $user = new UserModel();
    $user->name = 'John Doe';
    $user->email = 'john@example.com';
    $user->setPassword('password123');
    
    $result = $user->createModel();
    
    expect($result->response_code)->toBe(201)
        ->and($result->message)->toBe('created')
        ->and($user->id)->not->toBeNull();
});

test('selects user by email', function () {
    $userTable = new UserTable();
    $user = $userTable->selectByEmail('john@example.com');
    
    expect($user)->toBeInstanceOf(UserModel::class)
        ->and($user->email)->toBe('john@example.com');
});

test('prevents duplicate email', function () {
    $user1 = new UserModel();
    $user1->name = 'User 1';
    $user1->email = 'duplicate@example.com';
    $user1->setPassword('pass123');
    $user1->createModel();
    
    $user2 = new UserModel();
    $user2->name = 'User 2';
    $user2->email = 'duplicate@example.com';
    $user2->setPassword('pass123');
    $result = $user2->createModel();
    
    expect($result->response_code)->toBe(422)
        ->and($result->service_message)->toContain('already exists');
});

Pest Advantages:

  • โœ“ Elegant Syntax - More readable test code
  • โœ“ Built on PHPUnit - All PHPUnit features available
  • โœ“ Better DX - Improved developer experience
  • โœ“ Fluent Assertions - Chainable expectations
  • โœ“ Modern PHP - Uses latest PHP features
6

Testing Best Practices

How to Use Testing Tools

Follow these best practices for effective testing and code quality:

๐Ÿ’ก Use PHPStan During Development

Run PHPStan regularly while developing, not just before commits:

Terminal
# Run PHPStan frequently
vendor/bin/phpstan analyse

# Fix errors immediately
# Don't accumulate type errors

Tip: Set up a file watcher or run PHPStan in your IDE to catch errors as you type!

๐Ÿ’ก Write Tests for Critical Paths

Focus on testing business logic, API endpoints, and critical workflows. Don't test framework code - test your application code.

๐Ÿ’ก Test the 4-Layer Architecture

Test each layer appropriately:

  • โ€ข API Layer - Test schema validation, authentication
  • โ€ข Controller Layer - Test business logic orchestration
  • โ€ข Model Layer - Test data validations, transformations
  • โ€ข Table Layer - Test database queries (use test database!)

๐Ÿ’ก Use Test Database

Always use a separate test database. Never run tests against production data! Configure test database in phpunit.xml or Pest.php.

๐Ÿ’ก Run Tests Before Commits

Make it a habit to run both PHPStan and your test suite before committing code. This prevents broken code from reaching the repository.

Complete Testing Workflow

Here's a complete example showing how to use PHPStan and PHPUnit/Pest together:

Step 1: Write Code with Type Hints

app/model/ProductModel.php - Type-Safe Code
<?php
namespace App\Model;

use App\Table\ProductTable;
use Gemvc\Http\JsonResponse;
use Gemvc\Http\Response;

class ProductModel extends ProductTable
{
    public int $id;
    public string $name;
    public float $price;
    
    public function createModel(): JsonResponse
    {
        // Business validation
        if (empty($this->name)) {
            return Response::unprocessableEntity('Name is required');
        }
        
        if ($this->price <= 0) {
            return Response::unprocessableEntity('Price must be greater than 0');
        }
        
        // Database operation
        $this->insertSingleQuery();
        if ($this->getError()) {
            return Response::internalError($this->getError());
        }
        
        return Response::created($this, 1, 'Product created successfully');
    }
}

Step 2: Run PHPStan

Terminal
# Check code quality
vendor/bin/phpstan analyse

# PHPStan will verify:
# โœ“ All type hints are correct
# โœ“ No null pointer exceptions
# โœ“ All return types match
# โœ“ No undefined method calls

Step 3: Write Tests

tests/ProductModelTest.php - Test Example
<?php
namespace Tests;

use PHPUnit\Framework\TestCase;
use App\Model\ProductModel;

class ProductModelTest extends TestCase
{
    public function testCreateProductSuccess(): void
    {
        $product = new ProductModel();
        $product->name = 'Laptop';
        $product->price = 999.99;
        
        $result = $product->createModel();
        
        $this->assertEquals(201, $result->response_code);
        $this->assertEquals('created', $result->message);
        $this->assertNotNull($product->id);
    }
    
    public function testCreateProductWithoutName(): void
    {
        $product = new ProductModel();
        $product->price = 999.99;
        // name is empty
        
        $result = $product->createModel();
        
        $this->assertEquals(422, $result->response_code);
        $this->assertStringContainsString('Name is required', $result->service_message);
    }
    
    public function testCreateProductWithInvalidPrice(): void
    {
        $product = new ProductModel();
        $product->name = 'Laptop';
        $product->price = -100;  // Invalid price
        
        $result = $product->createModel();
        
        $this->assertEquals(422, $result->response_code);
        $this->assertStringContainsString('Price must be greater than 0', $result->service_message);
    }
}

Step 4: Run Tests

Terminal
# Run all tests
vendor/bin/phpunit

# Or with Pest
vendor/bin/pest

# Verify all tests pass
# โœ“ testCreateProductSuccess
# โœ“ testCreateProductWithoutName
# โœ“ testCreateProductWithInvalidPrice

Important Notes

  • PHPStan is Strongly Recommended: GEMVC is one of the few frameworks that supports PHPStan Level 9. Use it during development to catch type errors before they reach production!
  • Automatic Configuration: When you choose PHPStan or testing frameworks during gemvc init, they are automatically installed and configured. No manual setup needed!
  • Use During Development: Don't wait until the end to run PHPStan. Use it continuously during development to catch errors as you write code.
  • Test Database: Always use a separate test database. Never run tests against production data! Configure it in your test framework configuration file.
  • Level 9 Advantage: GEMVC's clean architecture allows PHPStan Level 9, which other frameworks can't achieve due to magic methods and dynamic properties.

๐Ÿงช Testing & Quality Complete!

Excellent! You've learned how to use PHPStan Level 9, PHPUnit, and Pest in GEMVC. Remember: GEMVC is one of the few frameworks that supports PHPStan Level 9 - use it during development to write type-safe, bug-free code!