Skip to content

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 test

TIP

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.php suffix
  • Test classes end with Test suffix
  • Test methods start with test prefix
  • One test class per source class

Base Classes ​

Choose the appropriate base class depending on what you need:

Base ClassUse When
PHPUnit\Framework\TestCasePure unit tests with no framework dependencies
Symfony\Bundle\FrameworkBundle\Test\KernelTestCaseTests that need the service container
Symfony\Bundle\FrameworkBundle\Test\WebTestCaseTests 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:

CheckCI ModeRequirement
PHPUnit testsfull + fastAll tests must pass
PHPStanfull + fastMust pass at configured level
php-cs-fixerfull onlyCode must match PSR-2/PSR-12 style
  • main branch and PRs to main run in full mode with all checks.
  • develop branch 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_vendors flag 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 --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 ​

Internal developer documentation for Menzzo systems.