typo3-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TYPO3 Testing Skill

TYPO3 测试技能

Comprehensive testing infrastructure for TYPO3 extensions: unit, functional, E2E, architecture, and mutation testing.
TYPO3 API First: Always use TYPO3's built-in APIs, core features, and established conventions before creating custom implementations. Do not reinvent what TYPO3 already provides. Always verify that the APIs and methods you use exist and are not deprecated in your target TYPO3 version (v13 or v14) by checking the official TYPO3 documentation.
面向TYPO3扩展的全面测试基础设施:包含单元测试、功能测试、端到端(E2E)测试、架构测试以及变异测试。
TYPO3 API优先原则: 在创建自定义实现之前,请始终使用TYPO3的内置API、核心功能和既定规范。不要重复造轮子,TYPO3已提供的功能直接使用即可。请务必通过官方TYPO3文档验证你所使用的API和方法在目标TYPO3版本(v13或v14)中存在且未被弃用。

Test Type Selection

测试类型选择

TypeUse WhenSpeedFramework
UnitPure logic, validators, utilitiesFast (ms)PHPUnit
FunctionalDB interactions, repositoriesMedium (s)PHPUnit + TYPO3
ArchitectureLayer constraints, dependenciesFast (ms)PHPat
E2EUser workflows, browserSlow (s-min)Playwright
MutationTest quality verificationCI onlyInfection
类型适用场景执行速度使用框架
单元测试纯逻辑、验证器、工具类快(毫秒级)PHPUnit
功能测试数据库交互、仓库层中等(秒级)PHPUnit + TYPO3
架构测试分层约束、依赖关系快(毫秒级)PHPat
端到端测试(E2E)用户工作流、浏览器交互慢(秒-分钟级)Playwright
变异测试测试质量验证仅CI环境使用Infection

Test Infrastructure Setup

测试基础设施搭建

Directory Structure

目录结构

Tests/
├── Functional/
│   ├── Controller/
│   ├── Repository/
│   └── Fixtures/
├── Unit/
│   ├── Service/
│   └── Validator/
├── Architecture/
│   └── ArchitectureTest.php
└── E2E/
    └── playwright/
Tests/
├── Functional/
│   ├── Controller/
│   ├── Repository/
│   └── Fixtures/
├── Unit/
│   ├── Service/
│   └── Validator/
├── Architecture/
│   └── ArchitectureTest.php
└── E2E/
    └── playwright/

PHPUnit Configuration

PHPUnit 配置

xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/typo3/testing-framework/Resources/Core/Build/UnitTestsBootstrap.php"
         colors="true"
         cacheResult="false">
    <testsuites>
        <testsuite name="Unit">
            <directory>Tests/Unit</directory>
        </testsuite>
        <testsuite name="Functional">
            <directory>Tests/Functional</directory>
        </testsuite>
        <testsuite name="Architecture">
            <directory>Tests/Architecture</directory>
        </testsuite>
    </testsuites>
    
    <coverage>
        <report>
            <clover outputFile="var/log/coverage.xml"/>
            <html outputDirectory="var/log/coverage"/>
        </report>
    </coverage>
    
    <source>
        <include>
            <directory>Classes</directory>
        </include>
        <exclude>
            <directory>Classes/Domain/Model</directory>
        </exclude>
    </source>
</phpunit>
xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/typo3/testing-framework/Resources/Core/Build/UnitTestsBootstrap.php"
         colors="true"
         cacheResult="false">
    <testsuites>
        <testsuite name="Unit">
            <directory>Tests/Unit</directory>
        </testsuite>
        <testsuite name="Functional">
            <directory>Tests/Functional</directory>
        </testsuite>
        <testsuite name="Architecture">
            <directory>Tests/Architecture</directory>
        </testsuite>
    </testsuites>
    
    <coverage>
        <report>
            <clover outputFile="var/log/coverage.xml"/>
            <html outputDirectory="var/log/coverage"/>
        </report>
    </coverage>
    
    <source>
        <include>
            <directory>Classes</directory>
        </include>
        <exclude>
            <directory>Classes/Domain/Model</directory>
        </exclude>
    </source>
</phpunit>

Functional Test Configuration

功能测试配置

xml
<!-- FunctionalTests.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
         colors="true">
    <testsuites>
        <testsuite name="Functional">
            <directory>Tests/Functional</directory>
        </testsuite>
    </testsuites>
</phpunit>
xml
<!-- FunctionalTests.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
         colors="true">
    <testsuites>
        <testsuite name="Functional">
            <directory>Tests/Functional</directory>
        </testsuite>
    </testsuites>
</phpunit>

Unit Testing

单元测试

Basic Unit Test

基础单元测试示例

php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Unit\Service;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Vendor\MyExtension\Service\PriceCalculator;

final class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $subject;

    protected function setUp(): void
    {
        parent::setUp();
        $this->subject = new PriceCalculator();
    }

    #[Test]
    public function calculateNetPriceReturnsCorrectValue(): void
    {
        $grossPrice = 119.00;
        $taxRate = 19.0;

        $netPrice = $this->subject->calculateNetPrice($grossPrice, $taxRate);

        self::assertEqualsWithDelta(100.00, $netPrice, 0.01);
    }

    #[Test]
    public function calculateNetPriceThrowsExceptionForNegativePrice(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionCode(1234567890);

        $this->subject->calculateNetPrice(-10.00, 19.0);
    }
}
php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Unit\Service;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Vendor\MyExtension\Service\PriceCalculator;

final class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $subject;

    protected function setUp(): void
    {
        parent::setUp();
        $this->subject = new PriceCalculator();
    }

    #[Test]
    public function calculateNetPriceReturnsCorrectValue(): void
    {
        $grossPrice = 119.00;
        $taxRate = 19.0;

        $netPrice = $this->subject->calculateNetPrice($grossPrice, $taxRate);

        self::assertEqualsWithDelta(100.00, $netPrice, 0.01);
    }

    #[Test]
    public function calculateNetPriceThrowsExceptionForNegativePrice(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionCode(1234567890);

        $this->subject->calculateNetPrice(-10.00, 19.0);
    }
}

Mocking Dependencies

依赖项模拟

php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Unit\Service;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Vendor\MyExtension\Service\ItemService;
use Vendor\MyExtension\Domain\Repository\ItemRepository;

final class ItemServiceTest extends TestCase
{
    private ItemRepository&MockObject $itemRepositoryMock;
    private LoggerInterface&MockObject $loggerMock;
    private ItemService $subject;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->itemRepositoryMock = $this->createMock(ItemRepository::class);
        $this->loggerMock = $this->createMock(LoggerInterface::class);
        
        $this->subject = new ItemService(
            $this->itemRepositoryMock,
            $this->loggerMock,
        );
    }

    #[Test]
    public function findActiveItemsReturnsFilteredItems(): void
    {
        $items = [/* mock items */];
        $this->itemRepositoryMock
            ->expects(self::once())
            ->method('findByActive')
            ->with(true)
            ->willReturn($items);

        $result = $this->subject->findActiveItems();

        self::assertSame($items, $result);
    }
}
php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Unit\Service;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Vendor\MyExtension\Service\ItemService;
use Vendor\MyExtension\Domain\Repository\ItemRepository;

final class ItemServiceTest extends TestCase
{
    private ItemRepository&MockObject $itemRepositoryMock;
    private LoggerInterface&MockObject $loggerMock;
    private ItemService $subject;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->itemRepositoryMock = $this->createMock(ItemRepository::class);
        $this->loggerMock = $this->createMock(LoggerInterface::class);
        
        $this->subject = new ItemService(
            $this->itemRepositoryMock,
            $this->loggerMock,
        );
    }

    #[Test]
    public function findActiveItemsReturnsFilteredItems(): void
    {
        $items = [/* mock items */];
        $this->itemRepositoryMock
            ->expects(self::once())
            ->method('findByActive')
            ->with(true)
            ->willReturn($items);

        $result = $this->subject->findActiveItems();

        self::assertSame($items, $result);
    }
}

Functional Testing

功能测试

Repository Test

仓库层测试示例

php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Functional\Repository;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\MyExtension\Domain\Repository\ItemRepository;

final class ItemRepositoryTest extends FunctionalTestCase
{
    protected array $testExtensionsToLoad = [
        'typo3conf/ext/my_extension',
    ];

    private ItemRepository $subject;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->importCSVDataSet(__DIR__ . '/Fixtures/Items.csv');
        $this->subject = $this->get(ItemRepository::class);
    }

    #[Test]
    public function findByUidReturnsCorrectItem(): void
    {
        $item = $this->subject->findByUid(1);

        self::assertNotNull($item);
        self::assertSame('Test Item', $item->getTitle());
    }

    #[Test]
    public function findAllReturnsAllItems(): void
    {
        $items = $this->subject->findAll();

        self::assertCount(3, $items);
    }
}
php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Functional\Repository;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\MyExtension\Domain\Repository\ItemRepository;

final class ItemRepositoryTest extends FunctionalTestCase
{
    protected array $testExtensionsToLoad = [
        'typo3conf/ext/my_extension',
    ];

    private ItemRepository $subject;

    protected function setUp(): void
    {
        parent::setUp();
        
        $this->importCSVDataSet(__DIR__ . '/Fixtures/Items.csv');
        $this->subject = $this->get(ItemRepository::class);
    }

    #[Test]
    public function findByUidReturnsCorrectItem(): void
    {
        $item = $this->subject->findByUid(1);

        self::assertNotNull($item);
        self::assertSame('Test Item', $item->getTitle());
    }

    #[Test]
    public function findAllReturnsAllItems(): void
    {
        $items = $this->subject->findAll();

        self::assertCount(3, $items);
    }
}

CSV Fixture Format

CSV 测试数据格式

csv
undefined
csv
undefined

Tests/Functional/Repository/Fixtures/Items.csv

Tests/Functional/Repository/Fixtures/Items.csv

"tx_myext_items" ,"uid","pid","title","active","deleted" ,1,1,"Test Item",1,0 ,2,1,"Another Item",1,0 ,3,1,"Inactive Item",0,0
undefined
"tx_myext_items" ,"uid","pid","title","active","deleted" ,1,1,"Test Item",1,0 ,2,1,"Another Item",1,0 ,3,1,"Inactive Item",0,0
undefined

Controller Functional Test

控制器功能测试示例

php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Functional\Controller;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\MyExtension\Controller\ItemController;

final class ItemControllerTest extends FunctionalTestCase
{
    protected array $testExtensionsToLoad = [
        'typo3conf/ext/my_extension',
    ];

    #[Test]
    public function listActionReturnsHtmlResponse(): void
    {
        $this->importCSVDataSet(__DIR__ . '/Fixtures/Pages.csv');
        $this->importCSVDataSet(__DIR__ . '/Fixtures/Items.csv');

        $request = new ServerRequest('https://example.com/items', 'GET');
        $controller = $this->get(ItemController::class);

        $response = $controller->listAction();

        self::assertSame(200, $response->getStatusCode());
        self::assertStringContainsString('text/html', $response->getHeaderLine('Content-Type'));
    }
}
php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Functional\Controller;

use PHPUnit\Framework\Attributes\Test;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
use Vendor\MyExtension\Controller\ItemController;

final class ItemControllerTest extends FunctionalTestCase
{
    protected array $testExtensionsToLoad = [
        'typo3conf/ext/my_extension',
    ];

    #[Test]
    public function listActionReturnsHtmlResponse(): void
    {
        $this->importCSVDataSet(__DIR__ . '/Fixtures/Pages.csv');
        $this->importCSVDataSet(__DIR__ . '/Fixtures/Items.csv');

        $request = new ServerRequest('https://example.com/items', 'GET');
        $controller = $this->get(ItemController::class);

        $response = $controller->listAction();

        self::assertSame(200, $response->getStatusCode());
        self::assertStringContainsString('text/html', $response->getHeaderLine('Content-Type'));
    }
}

Architecture Testing with PHPat

基于PHPat的架构测试

Installation

安装方法

bash
composer require --dev phpat/phpat
bash
composer require --dev phpat/phpat

Architecture Test

架构测试示例

php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Architecture;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class ArchitectureTest
{
    public function testDomainModelsShouldNotDependOnInfrastructure(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Domain\Model'))
            ->shouldNotDependOn()
            ->classes(
                Selector::inNamespace('Vendor\MyExtension\Controller'),
                Selector::inNamespace('Vendor\MyExtension\Infrastructure'),
            );
    }

    public function testServicesShouldNotDependOnControllers(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Service'))
            ->shouldNotDependOn()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Controller'));
    }

    public function testRepositoriesShouldImplementInterface(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::classname('/.*Repository$/', true))
            ->excluding(Selector::classname('/.*Interface$/', true))
            ->shouldImplement()
            ->classes(Selector::classname('/.*RepositoryInterface$/', true));
    }

    public function testOnlyServicesCanAccessRepositories(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Domain\Repository'))
            ->canOnlyBeAccessedBy()
            ->classes(
                Selector::inNamespace('Vendor\MyExtension\Service'),
                Selector::inNamespace('Vendor\MyExtension\Tests'),
            );
    }
}
php
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Tests\Architecture;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class ArchitectureTest
{
    public function testDomainModelsShouldNotDependOnInfrastructure(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Domain\Model'))
            ->shouldNotDependOn()
            ->classes(
                Selector::inNamespace('Vendor\MyExtension\Controller'),
                Selector::inNamespace('Vendor\MyExtension\Infrastructure'),
            );
    }

    public function testServicesShouldNotDependOnControllers(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Service'))
            ->shouldNotDependOn()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Controller'));
    }

    public function testRepositoriesShouldImplementInterface(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::classname('/.*Repository$/', true))
            ->excluding(Selector::classname('/.*Interface$/', true))
            ->shouldImplement()
            ->classes(Selector::classname('/.*RepositoryInterface$/', true));
    }

    public function testOnlyServicesCanAccessRepositories(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('Vendor\MyExtension\Domain\Repository'))
            ->canOnlyBeAccessedBy()
            ->classes(
                Selector::inNamespace('Vendor\MyExtension\Service'),
                Selector::inNamespace('Vendor\MyExtension\Tests'),
            );
    }
}

PHPat Configuration

PHPat 配置

neon
undefined
neon
undefined

phpstan.neon

phpstan.neon

includes: - vendor/phpat/phpat/extension.neon
parameters: level: 9 paths: - Classes - Tests
undefined
includes: - vendor/phpat/phpat/extension.neon
parameters: level: 9 paths: - Classes - Tests
undefined

E2E Testing with Playwright

基于Playwright的端到端(E2E)测试

Setup

环境搭建

bash
undefined
bash
undefined

Install Playwright

Install Playwright

npm init playwright@latest
npm init playwright@latest

Configure for TYPO3

Configure for TYPO3

mkdir -p Tests/E2E/playwright
undefined
mkdir -p Tests/E2E/playwright
undefined

Playwright Configuration

Playwright 配置

typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './Tests/E2E',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: process.env.BASE_URL || 'https://my-extension.ddev.site',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});
typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './Tests/E2E',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: process.env.BASE_URL || 'https://my-extension.ddev.site',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

E2E Test Example

E2E 测试示例

typescript
// Tests/E2E/item-list.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Item List', () => {
  test('displays items correctly', async ({ page }) => {
    await page.goto('/items');

    await expect(page.locator('h1')).toContainText('Items');
    await expect(page.locator('.item-card')).toHaveCount(3);
  });

  test('filters items by category', async ({ page }) => {
    await page.goto('/items');

    await page.selectOption('[data-testid="category-filter"]', 'electronics');
    await expect(page.locator('.item-card')).toHaveCount(1);
  });

  test('creates new item', async ({ page }) => {
    await page.goto('/items/new');

    await page.fill('[name="title"]', 'New Test Item');
    await page.fill('[name="description"]', 'Test description');
    await page.click('[type="submit"]');

    await expect(page).toHaveURL(/\/items\/\d+/);
    await expect(page.locator('h1')).toContainText('New Test Item');
  });
});
typescript
// Tests/E2E/item-list.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Item List', () => {
  test('displays items correctly', async ({ page }) => {
    await page.goto('/items');

    await expect(page.locator('h1')).toContainText('Items');
    await expect(page.locator('.item-card')).toHaveCount(3);
  });

  test('filters items by category', async ({ page }) => {
    await page.goto('/items');

    await page.selectOption('[data-testid="category-filter"]', 'electronics');
    await expect(page.locator('.item-card')).toHaveCount(1);
  });

  test('creates new item', async ({ page }) => {
    await page.goto('/items/new');

    await page.fill('[name="title"]', 'New Test Item');
    await page.fill('[name="description"]', 'Test description');
    await page.click('[type="submit"]');

    await expect(page).toHaveURL(/\/items\/\d+/);
    await expect(page.locator('h1')).toContainText('New Test Item');
  });
});

Mutation Testing with Infection

基于Infection的变异测试

Installation

安装方法

bash
composer require --dev infection/infection
bash
composer require --dev infection/infection

Configuration

配置文件

json
// infection.json5
{
    "$schema": "vendor/infection/infection/resources/schema.json",
    "source": {
        "directories": ["Classes"],
        "excludes": ["Domain/Model"]
    },
    "logs": {
        "text": "var/log/infection.log",
        "html": "var/log/infection.html"
    },
    "mutators": {
        "@default": true
    },
    "minMsi": 70,
    "minCoveredMsi": 80
}
json
// infection.json5
{
    "$schema": "vendor/infection/infection/resources/schema.json",
    "source": {
        "directories": ["Classes"],
        "excludes": ["Domain/Model"]
    },
    "logs": {
        "text": "var/log/infection.log",
        "html": "var/log/infection.html"
    },
    "mutators": {
        "@default": true
    },
    "minMsi": 70,
    "minCoveredMsi": 80
}

Run Mutation Tests

运行变异测试

bash
vendor/bin/infection --threads=4
bash
vendor/bin/infection --threads=4

Test Commands

测试命令汇总

bash
undefined
bash
undefined

Unit tests

Unit tests

vendor/bin/phpunit -c Tests/UnitTests.xml
vendor/bin/phpunit -c Tests/UnitTests.xml

Functional tests

Functional tests

vendor/bin/phpunit -c Tests/FunctionalTests.xml
vendor/bin/phpunit -c Tests/FunctionalTests.xml

Architecture tests

Architecture tests

vendor/bin/phpstan analyse
vendor/bin/phpstan analyse

All tests with coverage

All tests with coverage

vendor/bin/phpunit --coverage-html var/log/coverage
vendor/bin/phpunit --coverage-html var/log/coverage

E2E tests

E2E tests

npx playwright test
npx playwright test

Mutation tests

Mutation tests

vendor/bin/infection
undefined
vendor/bin/infection
undefined

CI/CD Configuration

CI/CD 配置示例

yaml
undefined
yaml
undefined

.github/workflows/tests.yml

.github/workflows/tests.yml

name: Tests
on: [push, pull_request]
jobs: unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' coverage: xdebug - run: composer install - run: vendor/bin/phpunit -c Tests/UnitTests.xml --coverage-clover coverage.xml
functional: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: test ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' - run: composer install - run: vendor/bin/phpunit -c Tests/FunctionalTests.xml
architecture: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' - run: composer install - run: vendor/bin/phpstan analyse
undefined
name: Tests
on: [push, pull_request]
jobs: unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' coverage: xdebug - run: composer install - run: vendor/bin/phpunit -c Tests/UnitTests.xml --coverage-clover coverage.xml
functional: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: test ports: - 3306:3306 steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' - run: composer install - run: vendor/bin/phpunit -c Tests/FunctionalTests.xml
architecture: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' - run: composer install - run: vendor/bin/phpstan analyse
undefined

Scoring Requirements

测试评分要求

CriterionRequirement
Unit testsRequired, 70%+ coverage
Functional testsRequired for DB operations
Architecture testsPHPat required for full conformance
PHPStanLevel 9+ (level 10 recommended)
E2E testsOptional, bonus points
Mutation70%+ MSI for bonus points

评估项要求
单元测试必须覆盖,代码覆盖率≥70%
功能测试涉及数据库操作的功能必须覆盖
架构测试必须使用PHPat以确保完全符合规范
PHPStan规则等级≥9(推荐等级10)
E2E测试可选,完成可获得额外加分
变异测试MSI≥70%可获得额外加分

Credits & Attribution

致谢与贡献

Thanks to Netresearch DTT GmbH for their contributions to the TYPO3 community.
感谢Netresearch DTT GmbH为TYPO3社区所做的贡献。