Appearance
Testing Guide
This guide covers writing and running tests in Logidav, including test structure, mocking patterns, and CI expectations.
Running Tests
bash
# Full test suite
SYMFONY_DEPRECATIONS_HELPER=weak_vendors vendor/bin/simple-phpunit -c phpunit.xml.dist
# Single test file
vendor/bin/simple-phpunit tests/AppBundle/Services/SaleServiceTest.php
# Single test method
vendor/bin/simple-phpunit --filter testCalculateTotalWithDiscount tests/AppBundle/Services/SaleServiceTest.php
# Via Makefile
make testTIP
The SYMFONY_DEPRECATIONS_HELPER=weak_vendors environment variable suppresses deprecation warnings from third-party vendor code. Always use it when running tests locally to reduce noise.
Test Structure
Tests mirror the src/ directory structure under tests/:
tests/
├── AppBundle/
│ ├── Command/
│ │ └── Sale/
│ │ └── ProcessSaleCommandTest.php
│ └── Services/
│ └── SaleServiceTest.php
├── CoreBundle/
│ ├── Api/
│ │ └── DpdApiTest.php
│ └── Services/
│ └── DpdServiceTest.php
└── MeduseBundle/
└── Services/
└── Queue/
└── OrderProcessorTest.phpNaming conventions:
- Test files end with
Test.phpsuffix - Test classes end with
Testsuffix - Test methods start with
testprefix - One test class per source class
Base Classes
Choose the appropriate base class depending on what you need:
| Base Class | Use When |
|---|---|
PHPUnit\Framework\TestCase | Pure unit tests with no framework dependencies |
Symfony\Bundle\FrameworkBundle\Test\KernelTestCase | Tests that need the service container |
Symfony\Bundle\FrameworkBundle\Test\WebTestCase | Tests that need the HTTP client |
php
<?php
// tests/AppBundle/Services/SaleServiceTest.php
namespace Tests\AppBundle\Services;
use AppBundle\Services\SaleService;
use PHPUnit\Framework\TestCase;
class SaleServiceTest extends TestCase
{
private $saleService;
private $em;
private $logger;
protected function setUp()
{
$this->em = $this->createMock(\Doctrine\ORM\EntityManagerInterface::class);
$this->logger = $this->createMock(\Psr\Log\LoggerInterface::class);
$this->saleService = new SaleService($this->em, $this->logger);
}
public function testCalculateTotalReturnsCorrectAmount()
{
$items = [
['price' => 10.00, 'quantity' => 2],
['price' => 25.50, 'quantity' => 1],
];
$result = $this->saleService->calculateTotal($items);
$this->assertEquals(45.50, $result);
}
}What to Test
Business Invariants
Test status transitions, validation rules, and calculation logic -- the core rules that must never break:
php
public function testOrderCannotTransitionFromShippedToPending()
{
$this->expectException(\InvalidArgumentException::class);
$this->saleService->updateStatus($order, 'pending');
}API Client Responses
Mock the HTTP layer and test that your API client correctly parses responses and handles errors:
php
public function testGetTrackingReturnsTrackingInfo()
{
$api = $this->getMockBuilder(DpdApi::class)
->disableOriginalConstructor()
->getMock();
$api->method('request')
->willReturn(json_encode(['tracking_number' => 'DPD123', 'status' => 'delivered']));
$result = $api->getTracking('DPD123');
$this->assertEquals('delivered', $result['status']);
}Queue Processor Logic
Test processors with mock queue entries to verify correct handling of success and failure:
php
public function testProcessorHandlesFailedEntryGracefully()
{
$queue = $this->createMock(Queue::class);
$queue->method('getId')->willReturn(1);
$queue->method('getPayload')->willReturn('invalid-json');
$results = $this->processor->execute([$queue]);
$this->assertEquals('error', $results[1]['status']);
}Command Execution
Use Symfony's CommandTester for testing console commands:
php
use Symfony\Component\Console\Tester\CommandTester;
public function testCommandOutputsResults()
{
$command = new SyncProductsCommand();
// Set up container mock on the command...
$tester = new CommandTester($command);
$tester->execute([]);
$this->assertContains('processed', $tester->getDisplay());
$this->assertEquals(0, $tester->getStatusCode());
}Mocking Patterns
Service Isolation
Always mock external dependencies. Never let unit tests call real APIs or databases:
php
// Mock EntityManager
$em = $this->createMock(EntityManagerInterface::class);
$em->expects($this->once())
->method('persist')
->with($this->isInstanceOf(Order::class));
$em->expects($this->once())
->method('flush');
// Mock Repository
$repo = $this->createMock(OrderRepository::class);
$repo->method('find')->with(42)->willReturn($mockOrder);
$em->method('getRepository')
->with(Order::class)
->willReturn($repo);Mocking API Clients
php
$api = $this->createMock(DpdApi::class);
// Simulate successful API response
$api->method('createShipment')
->willReturn(['shipment_id' => 'S123', 'label_url' => 'https://...']);
// Simulate API failure
$api->method('createShipment')
->willThrowException(new \RuntimeException('API timeout'));Testing with LockableTrait
Commands using LockableTrait create file-based locks. Handle this in tests:
php
protected function tearDown()
{
// Clean up any leftover lock files
$lockFile = sys_get_temp_dir() . '/sf.' . md5('your:command:name') . '.lock';
if (file_exists($lockFile)) {
unlink($lockFile);
}
}WARNING
Lock files persist across test runs if a test crashes. If you see "Command is already running" during tests, check for stale lock files in the system temp directory.
CI Expectations
Tests run automatically on Jenkins as part of the CI pipeline:
| Check | CI Mode | Requirement |
|---|---|---|
| PHPUnit tests | full + fast | All tests must pass |
| PHPStan | full + fast | Must pass at configured level |
| php-cs-fixer | full only | Code must match PSR-2/PSR-12 style |
mainbranch and PRs to main run in full mode with all checks.developbranch and PRs to develop run in fast mode with tests and basic PHPStan.- PHPStan runs with a 2G memory limit configured in CI.
- The
SYMFONY_DEPRECATIONS_HELPER=weak_vendorsflag is set in CI to match local behavior.
Before pushing, verify locally:
bash
# Run tests
SYMFONY_DEPRECATIONS_HELPER=weak_vendors vendor/bin/simple-phpunit -c phpunit.xml.dist
# Run PHPStan
vendor/bin/phpstan analyse
# Check code style
vendor/bin/php-cs-fixer fix --dry-run --diffChecklist for New Tests
- [ ] Test file mirrors the source file path under
tests/ - [ ] Test class extends the appropriate base class
- [ ] All external dependencies are mocked
- [ ] Both success and failure paths are covered
- [ ] Lock files are cleaned up in
tearDown()if testing locked commands - [ ] Tests pass locally before pushing
See Also
- CI/CD Pipeline for details on how tests run in CI
- CI/CD Pipeline for test architecture and CI expectations
- Adding a Queue Processor for queue processor test examples