Testing Guide
This guide covers writing and running tests in Logidav, including test structure, mocking patterns, and CI expectations.
Running Tests
# 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 test
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.php
Naming 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
// 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:
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:
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:
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:
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:
// 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
$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:
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);
}
}
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:
# 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 --diff
Checklist 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