Skip to content

PHPUnit Testing Best Practices in XOOPS

Testing is essential for ensuring code quality, preventing regressions, and enabling confident refactoring.

Installing PHPUnit

# Using Composer
composer require --dev phpunit/phpunit ^9.0

# Run tests
./vendor/bin/phpunit

phpunit.xml Configuration

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
         colors="true"
         verbose="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/integration</directory>
        </testsuite>
    </testsuites>

    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">class</directory>
        </include>
        <report>
            <html outputDirectory="coverage"/>
        </report>
    </coverage>
</phpunit>

Writing Unit Tests

<?php
namespace Xoops\Module\Mymodule\Tests\Unit;

use PHPUnit\Framework\TestCase;
use Xoops\Module\Mymodule\Service\UserService;

class UserServiceTest extends TestCase
{
    private $userService;
    private $mockRepository;

    protected function setUp(): void
    {
        parent::setUp();
        $this->mockRepository = $this->createMock(
            \Xoops\Module\Mymodule\Repository\UserRepositoryInterface::class
        );
        $this->userService = new UserService($this->mockRepository);
    }

    public function testRegisterSuccess()
    {
        // Arrange
        $this->mockRepository->expects($this->once())
            ->method('findByUsername')
            ->willReturn(null);

        $this->mockRepository->expects($this->once())
            ->method('save')
            ->willReturn(1);

        // Act
        $result = $this->userService->register('user', 'test@test.com', 'pass');

        // Assert
        $this->assertNotNull($result);
    }

    public function testRegisterDuplicate()
    {
        // Arrange
        $existingUser = new \stdClass();
        $this->mockRepository->expects($this->once())
            ->method('findByUsername')
            ->willReturn($existingUser);

        // Act & Assert
        $this->expectException(\Exception::class);
        $this->userService->register('user', 'test@test.com', 'pass');
    }
}
?>

Testing Data Objects

<?php
class UserDTOTest extends TestCase
{
    public function testDTOCreation()
    {
        $user = new User();
        $user->setId(1)
            ->setUsername('testuser')
            ->setEmail('test@test.com');

        $dto = new UserDTO($user);

        $this->assertEquals(1, $dto->getId());
        $this->assertEquals('testuser', $dto->getUsername());
    }

    public function testDTOToArray()
    {
        $user = new User();
        $user->setId(1)->setUsername('testuser');

        $dto = new UserDTO($user);
        $array = $dto->toArray();

        $this->assertIsArray($array);
        $this->assertEquals(1, $array['id']);
    }
}
?>

Code Coverage

# Generate coverage report
./vendor/bin/phpunit --coverage-html coverage

# View coverage percentage
./vendor/bin/phpunit --coverage-text

Best Practices

  • Write one test per method/scenario
  • Use descriptive test names
  • Follow Arrange-Act-Assert pattern
  • Mock external dependencies
  • Keep tests focused and independent
  • Aim for 80%+ code coverage
  • Test error conditions
  • Test boundary cases

Test Organization

tests/
├── unit/
│   ├── UserServiceTest.php
│   ├── UserRepositoryTest.php
│   └── UserDTOTest.php
├── integration/
│   ├── UserControllerTest.php
│   └── UserServiceTest.php
├── fixtures/
│   └── users.php
├── bootstrap.php
└── phpunit.xml

See also: - Error-Handling for exception testing - Repository-Pattern for repository testing - Service-Layer for service testing - Code-Organization for test structure


Tags: #best-practices #testing #phpunit #code-coverage #module-development