Zum Inhalt springen

Effective Testing with PHPUnit: Symfony Projects and Shopware 6 Plugins

Veröffentlicht am Jan 20, 2026 | ca. 1 Min. Lesezeit |

Tests are not an optional luxury — they are an investment in the quality and maintainability of your code. In Symfony projects, PHPUnit provides all the necessary tools for this.

The Test Pyramid

A healthy test structure follows the test pyramid:

  • Unit tests (many) — Test individual classes in isolation
  • Integration tests (some) — Test the interaction of multiple components
  • Functional tests (few) — Test complete HTTP requests

Unit Tests: Testing Services in Isolation

A service should be testable independently of infrastructure:

<?php

declare(strict_types=1);

namespace App\Tests\Service;

use App\Service\PriceCalculator;
use PHPUnit\Framework\TestCase;

class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $calculator;

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

    public function testCalculateNetPrice(): void
    {
        $grossPrice = 119.0;
        $taxRate = 19.0;

        $netPrice = $this->calculator->calculateNet($grossPrice, $taxRate);

        $this->assertEqualsWithDelta(100.0, $netPrice, 0.01);
    }

    public function testCalculateWithDiscount(): void
    {
        $grossPrice = 100.0;
        $discount = 10.0;

        $result = $this->calculator->applyDiscount($grossPrice, $discount);

        $this->assertEquals(90.0, $result);
    }
}

Integration Tests: Including the Database

For tests that require database access, Symfony provides the KernelTestCase:

<?php

declare(strict_types=1);

namespace App\Tests\Repository;

use App\Entity\Product;
use App\Repository\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class ProductRepositoryTest extends KernelTestCase
{
    private ProductRepository $repository;

    protected function setUp(): void
    {
        self::bootKernel();
        $this->repository = static::getContainer()
            ->get(ProductRepository::class);
    }

    public function testFindActiveProducts(): void
    {
        $products = $this->repository->findActive();

        $this->assertNotEmpty($products);
        foreach ($products as $product) {
            $this->assertTrue($product->isActive());
        }
    }
}

Functional Tests: Testing Controllers

With the WebTestCase you can simulate complete HTTP requests:

<?php

declare(strict_types=1);

namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ProductControllerTest extends WebTestCase
{
    public function testProductListReturns200(): void
    {
        $client = static::createClient();
        $client->request('GET', '/api/products');

        $this->assertResponseIsSuccessful();
        $this->assertResponseHeaderSame('content-type', 'application/json');
    }

    public function testCreateProductRequiresAuth(): void
    {
        $client = static::createClient();
        $client->request('POST', '/api/products');

        $this->assertResponseStatusCodeSame(401);
    }
}

Data Providers for Parameterized Tests

Data providers reduce duplication in similar test cases:

public static function invalidPriceProvider(): array
{
    return [
        'negative price' => [-10.0],
        'zero price' => [0.0],
        'extremely high' => [999999999.99],
    ];
}

#[\PHPUnit\Framework\Attributes\DataProvider('invalidPriceProvider')]
public function testRejectsInvalidPrices(float $price): void
{
    $this->expectException(\InvalidArgumentException::class);
    $this->calculator->validatePrice($price);
}

Shopware 6: Testing in Plugin Development

Shopware 6 also follows the test pyramid but uses its own test setup with specialized base classes and traits. The testing landscape looks like this:

Layer Framework Usage
PHP Backend PHPUnit Unit tests, integration tests, migration tests
Administration (Vue) Jest Admin component tests
Storefront JS Jest Storefront JavaScript tests
E2E / Acceptance Playwright End-to-end tests (since late 2023, replacing Cypress)

PHPUnit Setup for Plugins

Each Shopware plugin can include its own PHPUnit tests. The setup starts with a phpunit.xml in the plugin root and a bootstrap file that starts the Shopware kernel:

<?php

declare(strict_types=1);

use Shopware\Core\TestBootstrapper;

$loader = (new TestBootstrapper())
    ->addCallingPlugin()
    ->addActivePlugins('MeinPlugin')
    ->setForceInstallPlugins(true)
    ->bootstrap()
    ->getClassLoader();

$loader->addPsr4('MeinPlugin\\Tests\\', __DIR__);

setForceInstallPlugins(true) ensures that the plugin remains installed and active even when the test database already exists.

Integration Tests with IntegrationTestBehaviour

The IntegrationTestBehaviour trait is the workhorse for Shopware plugin tests. It provides automatic database transactions (rollback after each test), cache clearing, and container access:

<?php

declare(strict_types=1);

namespace MeinPlugin\Tests;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;

class MeinServiceTest extends TestCase
{
    use IntegrationTestBehaviour;

    public function testServiceIstRegistriert(): void
    {
        $service = $this->getContainer()->get(MeinService::class);

        $this->assertInstanceOf(MeinService::class, $service);
    }
}

For migration tests there is the leaner KernelTestBehaviour, which only provides container access without automatic transaction rollbacks.

Running Tests

# All plugin tests
vendor/bin/phpunit --configuration="custom/plugins/MeinPlugin"

# Only migration tests
vendor/bin/phpunit --configuration="custom/plugins/MeinPlugin" --testsuite "migration"

# Jest tests for the Administration
composer run admin:unit

# Jest tests for the Storefront
composer run storefront:unit

E2E Tests with Playwright

In late 2023, Shopware decided via ADR to migrate from Cypress to Playwright. The reasons: better performance, more stable tests, and built-in tracing for CI/CD. The old Cypress repository (@shopware-ag/e2e-testsuite-platform) has been archived.

The new acceptance test suite is based on Playwright and offers pre-built fixtures:

import { test, expect } from '@shopware-ag/acceptance-test-suite';

test('Produktanlage im Admin', async ({ ShopAdmin, AdminProductCreate }) => {
    await ShopAdmin.goesTo(AdminProductCreate.url());
    await ShopAdmin.expects(AdminProductCreate.nameInput).toBeVisible();
    await ShopAdmin.expects(AdminProductCreate.saveButton).toBeVisible();
});

test('Testdaten per API erstellen', async ({ TestDataService }) => {
    const product = await TestDataService.createProductWithImage({
        description: 'Test-Beschreibung'
    });
    expect(product.description).toEqual('Test-Beschreibung');
    expect(product.coverId).toBeDefined();
});

Setup:

npm init playwright@latest
npm install @shopware-ag/acceptance-test-suite
npx playwright install

The suite provides ShopAdmin and ShopCustomer actors, an AdminApiContext for API calls, and a TestDataService for programmatic test data creation.

Conclusion

Good tests give you confidence during refactoring and save significantly more time in the long run than they cost. Start with the most critical paths and gradually expand your coverage. For Shopware plugins, integration tests with IntegrationTestBehaviour are the best starting point — the trait takes care of most of the setup boilerplate for you.

Thomas Wunner

Thomas Wunner

Certified IT specialist for application development with an instructor qualification and over 14 years of experience building scalable web applications with Symfony and Shopware. When not coding, Thomas volunteers as a lifeguard with the Wasserwacht, performs as a DJ, and explores the countryside on his motorbike.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.