PHP项目Codeception全栈测试

wen PHP项目 1

本文目录导读:

PHP项目Codeception全栈测试

  1. Codeception安装与配置
  2. 配置文件示例
  3. 测试类型详解
  4. 高级测试模式
  5. 测试套件配置
  6. 自定义模块与Helper
  7. 持续集成配置
  8. 最佳实践

我来详细介绍PHP项目的Codeception全栈测试实现方案。

Codeception安装与配置

安装Codeception

# 通过Composer安装
composer require --dev codeception/codeception
# 初始化(选择项目类型)
vendor/bin/codecept init

项目结构

tests/
├── acceptance/      # 接受测试
├── functional/      # 功能测试
├── unit/           # 单元测试
├── _support/       # 辅助类
│   ├── AcceptanceTester.php
│   ├── FunctionalTester.php
│   └── UnitTester.php
├── _output/        # 测试输出
├── _data/          # 测试数据
└── codeception.yml # 主配置文件

配置文件示例

codeception.yml

namespace: Tests
paths:
    tests: tests
    output: tests/_output
    data: tests/_data
    support: tests/_support
    envs: tests/_envs
actor_suffix: Tester
extensions:
    enabled:
        - Codeception\Extension\RunFailed
# 数据库配置
params:
    - .env
# 模块配置
modules:
    config:
        Db:
            dsn: '%DB_DSN%'
            user: '%DB_USER%'
            password: '%DB_PASSWORD%'
            dump: tests/_data/dump.sql
            populate: true
            cleanup: true
            reconnect: true

测试类型详解

单元测试 (Unit Tests)

<?php
// tests/unit/UserServiceTest.php
class UserServiceTest extends \Codeception\Test\Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;
    private $userService;
    protected function _before()
    {
        // 模拟Repository
        $userRepository = $this->createMock(UserRepositoryInterface::class);
        $this->userService = new UserService($userRepository);
    }
    public function testCreateUser()
    {
        $userData = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'secure123'
        ];
        $result = $this->userService->create($userData);
        $this->assertInstanceOf(User::class, $result);
        $this->assertEquals($userData['name'], $result->getName());
    }
    public function testFindUserById()
    {
        $userId = 1;
        $this->tester->haveRecord('users', [
            'id' => $userId,
            'name' => 'John Doe',
            'email' => 'john@example.com'
        ]);
        $user = $this->userService->findById($userId);
        $this->assertNotNull($user);
        $this->assertEquals($userId, $user->getId());
    }
}

功能测试 (Functional Tests)

<?php
// tests/functional/UserControllerCest.php
class UserControllerCest
{
    public function _before(FunctionalTester $I)
    {
        // 数据库迁移
        $I->haveHttpHeader('X-API-Key', 'test-api-key');
        // 创建测试用户
        $I->haveRecord('users', [
            'id' => 1,
            'email' => 'test@example.com',
            'password' => password_hash('password123', PASSWORD_DEFAULT),
            'role' => 'admin'
        ]);
    }
    public function testGetUser(FunctionalTester $I)
    {
        $I->sendGET('/api/users/1');
        $I->seeResponseCodeIs(200);
        $I->seeResponseIsJson();
        $I->seeResponseContainsJson([
            'id' => 1,
            'email' => 'test@example.com'
        ]);
    }
    public function testCreateUser(FunctionalTester $I)
    {
        $I->sendPOST('/api/users', [
            'name' => 'New User',
            'email' => 'newuser@example.com',
            'password' => 'secure123'
        ]);
        $I->seeResponseCodeIs(201);
        $I->seeResponseIsJson();
        // 验证数据库
        $I->seeInDatabase('users', [
            'email' => 'newuser@example.com'
        ]);
    }
    public function testInvalidEmail(FunctionalTester $I)
    {
        $I->sendPOST('/api/users', [
            'email' => 'invalid-email',
            'password' => 'secure123'
        ]);
        $I->seeResponseCodeIs(422);
        $I->seeResponseContains('The email must be a valid email address.');
    }
}

接受测试 (Acceptance Tests)

<?php
// tests/acceptance/LoginCest.php
class LoginCest
{
    public function _before(AcceptanceTester $I)
    {
        // 创建测试数据
        $I->haveRecord('users', [
            'email' => 'user@example.com',
            'password' => password_hash('pass123', PASSWORD_DEFAULT),
            'name' => 'Test User'
        ]);
    }
    public function testSuccessfulLogin(AcceptanceTester $I)
    {
        $I->amOnPage('/login');
        $I->fillField('email', 'user@example.com');
        $I->fillField('password', 'pass123');
        $I->click('Login');
        $I->seeCurrentUrlEquals('/dashboard');
        $I->see('Welcome, Test User');
    }
    public function testFailedLogin(AcceptanceTester $I)
    {
        $I->amOnPage('/login');
        $I->fillField('email', 'wrong@example.com');
        $I->fillField('password', 'wrongpassword');
        $I->click('Login');
        $I->see('Invalid credentials');
        $I->seeCurrentUrlEquals('/login');
    }
    public function testFormValidation(AcceptanceTester $I)
    {
        $I->amOnPage('/login');
        $I->click('Login'); // 空表单提交
        $I->see('The email field is required');
        $I->see('The password field is required');
    }
}

高级测试模式

数据供给器 (Data Providers)

<?php
class MathTest extends \Codeception\Test\Unit
{
    /**
     * @dataProvider additionProvider
     */
    public function testAddition($a, $b, $expected)
    {
        $result = $a + $b;
        $this->assertEquals($expected, $result);
    }
    public function additionProvider()
    {
        return [
            'positive numbers' => [1, 2, 3],
            'negative numbers' => [-1, -2, -3],
            'zero values' => [0, 0, 0],
            'mixed values' => [5, -3, 2]
        ];
    }
}

依赖注入测试

<?php
class OrderServiceTest extends \Codeception\Test\Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;
    public function testOrderCreationWithDependencies()
    {
        // 模拟依赖
        $paymentGateway = $this->createMock(PaymentGatewayInterface::class);
        $notificationService = $this->createMock(NotificationServiceInterface::class);
        $paymentGateway->method('process')
            ->willReturn(new PaymentResponse(true, 'txn_123'));
        $notificationService->method('send')
            ->willReturn(true);
        $orderService = new OrderService($paymentGateway, $notificationService);
        $order = $orderService->create([
            'user_id' => 1,
            'items' => ['product_1' => 2],
            'total' => 100.00
        ]);
        $this->assertInstanceOf(Order::class, $order);
        $this->assertEquals('pending', $order->getStatus());
    }
}

API测试

<?php
// tests/api/ProductApiCest.php
class ProductApiCest
{
    public function testProductCRUD(ApiTester $I)
    {
        // 认证
        $I->haveHttpHeader('Authorization', 'Bearer ' . $this->getToken());
        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->haveHttpHeader('Accept', 'application/json');
        // CREATE
        $I->sendPOST('/api/products', [
            'name' => 'Test Product',
            'price' => 29.99,
            'category' => 'electronics'
        ]);
        $I->seeResponseCodeIs(201);
        $I->seeResponseIsJson();
        // 获取创建的产品ID
        $productId = $I->grabDataFromResponseByJsonPath('$.id')[0];
        // READ
        $I->sendGET('/api/products/' . $productId);
        $I->seeResponseCodeIs(200);
        $I->seeResponseMatchesJsonType([
            'id' => 'integer:>0',
            'name' => 'string',
            'price' => 'float'
        ]);
        // UPDATE
        $I->sendPUT('/api/products/' . $productId, [
            'price' => 19.99
        ]);
        $I->seeResponseCodeIs(200);
        $I->seeResponseContainsJson(['price' => 19.99]);
        // DELETE
        $I->sendDELETE('/api/products/' . $productId);
        $I->seeResponseCodeIs(204);
    }
}

测试套件配置

环境配置文件

# tests/_envs/staging.yml
modules:
    config:
        Db:
            dsn: 'mysql:host=staging-db;dbname=test'
        WebDriver:
            url: 'https://staging.example.com'

运行特定环境测试

# 运行特定环境
vendor/bin/codecept run --env staging
# 运行多个环境
vendor/bin/codecept run --env staging --env production
# 并行运行测试
vendor/bin/codecept run --parallel

自定义模块与Helper

创建自定义Helper

<?php
// tests/_support/Helper/CustomHelper.php
namespace Helper;
class CustomHelper extends \Codeception\Module
{
    public function loginAsUser($email, $password)
    {
        $webDriver = $this->getModule('WebDriver');
        $webDriver->amOnPage('/login');
        $webDriver->fillField('email', $email);
        $webDriver->fillField('password', $password);
        $webDriver->click('Login');
        return $this;
    }
    public function seeSuccessMessage($message)
    {
        $webDriver = $this->getModule('WebDriver');
        $webDriver->see($message, '.alert-success');
        return $this;
    }
    public function generateTestUser($overrides = [])
    {
        $defaults = [
            'name' => 'Test User ' . uniqid(),
            'email' => 'test_' . uniqid() . '@example.com',
            'password' => 'password123'
        ];
        return array_merge($defaults, $overrides);
    }
}

使用自定义Helper

<?php
class UserTest extends \Codeception\Test\Unit
{
    /**
     * @var \UnitTester
     */
    protected $tester;
    public function testUserRegistration()
    {
        $userData = $this->tester->generateTestUser([
            'role' => 'premium'
        ]);
        // 使用自定义断言
        $this->tester->assertValidEmail($userData['email']);
        $this->tester->assertStrongPassword($userData['password']);
    }
}

持续集成配置

GitHub Actions示例

# .github/workflows/test.yml
name: PHP Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s
    steps:
    - uses: actions/checkout@v2
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        extensions: mbstring, intl, pdo_mysql
        coverage: xdebug
    - name: Install dependencies
      run: composer install --prefer-dist --no-progress
    - name: Setup environment
      run: |
        cp .env.example .env
        php artisan key:generate
    - name: Run tests
      run: |
        vendor/bin/codecept build
        vendor/bin/codecept run --coverage --coverage-xml
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: tests/_output/coverage.xml

最佳实践

测试组织结构

// 使用标签组织测试
/**
 * @group api
 * @group critical
 */
public function testCriticalApiEndpoint()
{
    // 测试代码
}
// 运行特定标签的测试
// vendor/bin/codecept run --group api
// vendor/bin/codecept run --group critical --skip-group slow

测试数据管理

<?php
class TestDataFactory
{
    private static $data = [];
    public static function createUser($overrides = [])
    {
        $defaults = [
            'name' => 'Test User',
            'email' => self::uniqueEmail(),
            'password' => 'password123'
        ];
        $userData = array_merge($defaults, $overrides);
        self::$data['users'][] = $userData;
        return $userData;
    }
    public static function cleanup()
    {
        // 清理测试数据
        foreach (self::$data as $table => $records) {
            // 删除测试数据
        }
        self::$data = [];
    }
    private static function uniqueEmail()
    {
        return 'test_' . uniqid() . '@example.com';
    }
}

性能优化技巧

<?php
class PerformanceTest extends \Codeception\Test\Unit
{
    // 使用共享fixture
    private static $sharedFixtures = [];
    public static function setUpBeforeClass()
    {
        // 只创建一次共享数据
        self::$sharedFixtures = createExpensiveFixtures();
    }
    public function testFeature1()
    {
        // 使用共享数据
        $this->assertNotNull(self::$sharedFixtures['user']);
    }
    public function testFeature2()
    {
        // 重用共享数据
        $this->assertCount(10, self::$sharedFixtures['items']);
    }
}

调试技巧

# 调试模式运行测试
vendor/bin/codecept run --debug
# 暂停测试执行
vendor/bin/codecept run --steps
# 重建测试套件
vendor/bin/codecept build
# 生成测试报告
vendor/bin/codecept run --html --xml

这个全栈测试方案覆盖了Codeception的各种测试类型和最佳实践,能够帮助你在PHP项目中建立完整的测试体系。

抱歉,评论功能暂时关闭!