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.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.