Setting Up Tests
Install symfony/test-pack, learn what the test environment changes, and get a first PHPUnit run passing.
composer require --dev symfony/test-pack && php bin/phpunitA test is a small program that runs your code and fails loudly when the result is wrong. Symfony does not ship its own test runner. It ships PHPUnit, a standalone library, plus thin base classes that boot your application inside a test.
Installing
composer require --dev symfony/test-pack pulls in PHPUnit, the browser and DOM packages later lessons use, a config file, a tests/bootstrap.php file and a bin/phpunit wrapper. On Symfony 8.1 the generated config file is phpunit.dist.xml, the name PHPUnit 10 and later look for; older projects carry phpunit.xml.dist instead. bin/phpunit and vendor/bin/phpunit run the same binary, but the wrapper loads that bootstrap file first, which is what prepares the environment. Use the wrapper.
The test environment
Tests run under the environment name test. Symfony loads config/packages/test/ and any when@test block, so tests can use different services without disturbing development config. Variables come from .env.test, which is committed, and .env.test.local, which is not.
Your first green run
Put a file under tests/, name it SomethingTest.php, namespace it App\Tests\..., extend PHPUnit\Framework\TestCase, and give it public methods whose names start with test. Then run php bin/phpunit.
PHPUnit declares every assertion static, so self::assertSame() and $this->assertSame() are the same method called two ways. Either works; pick one and stay with it.
Example
<?php
// tests/Pricing/MoneyTest.php
declare(strict_types=1);
namespace App\Tests\Pricing;
use App\Pricing\Money;
use PHPUnit\Framework\TestCase;
final class MoneyTest extends TestCase
{
public function testAddingTwoAmountsKeepsTheCurrency(): void
{
$total = (new Money(1250, 'EUR'))->add(new Money(750, 'EUR'));
// self::assertSame() and $this->assertSame() are the same method.
// PHPUnit declares all assertions static.
self::assertSame(2000, $total->amountInCents());
$this->assertSame('EUR', $total->currency());
}
public function testAddingDifferentCurrenciesIsRejected(): void
{
$this->expectException(\InvalidArgumentException::class);
(new Money(1250, 'EUR'))->add(new Money(750, 'GBP'));
}
}
When to use it
- Add a test suite to a Symfony project that has never had one.
- Give a new contributor a single command that proves their checkout works.
- Separate test-only service configuration from development config using when@test.
- Run just the tests for the file you are editing instead of the whole suite.
More examples
The class under test
Nothing here touches the framework: no container, no request, no database. A class like this is the cheapest thing in the project to test, which is why it is the right place to start. Money amounts are held as integer cents so the assertions are exact rather than floating point.
<?php
// src/Pricing/Money.php
declare(strict_types=1);
namespace App\Pricing;
final class Money
{
public function __construct(
private readonly int $amountInCents,
private readonly string $currency,
) {
}
public function add(self $other): self
{
if ($other->currency !== $this->currency) {
throw new \InvalidArgumentException('Cannot add different currencies.');
}
return new self($this->amountInCents + $other->amountInCents, $this->currency);
}
public function amountInCents(): int
{
return $this->amountInCents;
}
public function currency(): string
{
return $this->currency;
}
}
// Output:
// (new Money(1250, 'EUR'))->add(new Money(750, 'EUR'))->amountInCents() === 2000
Running the suite
Each dot is a passing test method. F marks a failed assertion, E an uncaught error, S a skipped test. Narrowing the run to one file or directory is how you keep the feedback loop short while writing a single test; run the whole suite before you commit.
$ php bin/phpunit
# PHPUnit 12.0.0 by Sebastian Bergmann and contributors.
#
# Runtime: PHP 8.5.0
# Configuration: /srv/app/phpunit.dist.xml
#
# .. 2 / 2 (100%)
#
# Time: 00:00.041, Memory: 10.00 MB
#
# OK (2 tests, 3 assertions)
# One file only:
$ php bin/phpunit tests/Pricing/MoneyTest.php
# One directory:
$ php bin/phpunit tests/Pricing
Test environment variables
Values in .env.test apply whenever the environment is test, and they override .env. Anything secret or machine-specific belongs in .env.test.local, which is gitignored. Note that .env.local is deliberately skipped in the test environment, so a working development setup does not silently leak into the suite.
# .env.test
# Committed, so every developer and CI run shares these.
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
# A database name that is obviously not the development one.
DATABASE_URL="postgresql://app:[email protected]:5432/app_test?serverVersion=17&charset=utf8"
# Keep the error page out of the way so failures show the real exception.
APP_DEBUG=1
Discussion