🧪 Testing de modulos — PHPUnit + Behat en PrestaShop

Actualizado: 2025-01-15

#Tipos de tests en PS

TipoHerramientaQue testeaVelocidad
UnitPHPUnitLogica pura sin dependencias (calculos, validaciones)Muy rapido
IntegrationPHPUnit + BDInteraccion con BD, ObjectModel, ConfigurationMedio
FunctionalBehat + SeleniumFlujos completos: install, config, front hooksLento

#PHPUnit — configuracion

Estructura de tests en el modulo
text
modules/ecom_mimodulo/
├── tests/
│   ├── phpunit.xml              # Configuracion PHPUnit
│   ├── bootstrap.php            # Bootstrap para tests
│   ├── Unit/
│   │   ├── PriceCalculatorTest.php
│   │   └── ValidatorTest.php
│   ├── Integration/
│   │   ├── InstallTest.php
│   │   └── ConfigurationTest.php
│   └── Fixtures/
│       └── products.json
├── composer.json
└── ...
tests/phpunit.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
    bootstrap="bootstrap.php"
    colors="true"
    verbose="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>Integration</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory suffix=".php">../classes</directory>
            <directory suffix=".php">../src</directory>
        </include>
    </coverage>
    <php>
        <env name="PS_ROOT" value="/var/www/prestashop" />
    </php>
</phpunit>
tests/bootstrap.php
php
<?php
/**
 * Bootstrap para tests del modulo
 * Carga PrestaShop core para integration tests
 */

$psRoot = getenv('PS_ROOT') ?: '/var/www/prestashop';

// Para unit tests puros, no necesitamos PS core
if (getenv('UNIT_ONLY')) {
    // Autoloader del modulo
    require_once dirname(__DIR__) . '/vendor/autoload.php';
    return;
}

// Para integration tests, cargar PS
if (file_exists($psRoot . '/config/config.inc.php')) {
    require_once $psRoot . '/config/config.inc.php';
}

// Cargar clases del modulo
require_once dirname(__DIR__) . '/ecom_mimodulo.php';
if (file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
    require_once dirname(__DIR__) . '/vendor/autoload.php';
}

#Unit test: logica pura

tests/Unit/PriceCalculatorTest.php
php
<?php
namespace EcomMimodulo\Tests\Unit;

use PHPUnit\Framework\TestCase;
use EcomMimodulo\Service\PriceCalculator;

class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $calculator;

    protected function setUp(): void
    {
        $this->calculator = new PriceCalculator();
    }

    public function testCalculateDiscountPercent(): void
    {
        // 20% descuento sobre 100 EUR = 80 EUR
        $result = $this->calculator->applyDiscount(100.00, 20, 'percent');
        $this->assertEquals(80.00, $result);
    }

    public function testCalculateDiscountFixed(): void
    {
        // 15 EUR descuento sobre 100 EUR = 85 EUR
        $result = $this->calculator->applyDiscount(100.00, 15, 'amount');
        $this->assertEquals(85.00, $result);
    }

    public function testDiscountNeverNegative(): void
    {
        // Descuento mayor que precio = 0, nunca negativo
        $result = $this->calculator->applyDiscount(10.00, 50, 'amount');
        $this->assertEquals(0.00, $result);
    }

    public function testInvalidDiscountType(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->calculator->applyDiscount(100.00, 10, 'invalid');
    }

    /**
     * @dataProvider marginProvider
     */
    public function testCalculateMargin(float $price, float $cost, float $expected): void
    {
        $margin = $this->calculator->calculateMargin($price, $cost);
        $this->assertEquals($expected, $margin);
    }

    public function marginProvider(): array
    {
        return [
            'normal margin'     => [100.00, 60.00, 40.0],
            'zero margin'       => [50.00, 50.00, 0.0],
            'negative margin'   => [30.00, 50.00, -66.67],
            'zero cost'         => [100.00, 0.00, 100.0],
        ];
    }
}

#Integration test: con BD

tests/Integration/InstallTest.php
php
<?php
namespace EcomMimodulo\Tests\Integration;

use PHPUnit\Framework\TestCase;
use Ecom_mimodulo;
use Db;
use Configuration;

/**
 * Tests de integracion — requieren PS core y BD
 * Ejecutar: PS_ROOT=/var/www/prestashop phpunit --testsuite Integration
 */
class InstallTest extends TestCase
{
    private static Ecom_mimodulo $module;

    public static function setUpBeforeClass(): void
    {
        self::$module = new Ecom_mimodulo();
    }

    public function testModuleCanInstall(): void
    {
        // Desinstalar primero si ya esta instalado
        if (self::$module->isInstalled(self::$module->name)) {
            self::$module->uninstall();
        }

        $result = self::$module->install();
        $this->assertTrue($result, 'El modulo debe instalarse correctamente');
    }

    /**
     * @depends testModuleCanInstall
     */
    public function testTablesCreatedAfterInstall(): void
    {
        $tables = Db::getInstance()->executeS('SHOW TABLES LIKE \'' . _DB_PREFIX_ . 'ecom_mimodulo%\'');
        $this->assertNotEmpty($tables, 'Las tablas del modulo deben existir');
    }

    /**
     * @depends testModuleCanInstall
     */
    public function testDefaultConfigValues(): void
    {
        $this->assertEquals('1', Configuration::get('ECOM_MIMODULO_ENABLED'));
        $this->assertNotEmpty(Configuration::get('ECOM_MIMODULO_TITLE'));
    }

    /**
     * @depends testModuleCanInstall
     */
    public function testHooksRegistered(): void
    {
        $this->assertTrue(
            self::$module->isRegisteredInHook('displayHeader'),
            'displayHeader hook debe estar registrado'
        );
        $this->assertTrue(
            self::$module->isRegisteredInHook('actionCartSave'),
            'actionCartSave hook debe estar registrado'
        );
    }

    /**
     * @depends testModuleCanInstall
     */
    public function testModuleCanUninstall(): void
    {
        $result = self::$module->uninstall();
        $this->assertTrue($result, 'El modulo debe desinstalarse correctamente');
    }

    /**
     * @depends testModuleCanUninstall
     */
    public function testTablesRemovedAfterUninstall(): void
    {
        $tables = Db::getInstance()->executeS('SHOW TABLES LIKE \'' . _DB_PREFIX_ . 'ecom_mimodulo%\'');
        $this->assertEmpty($tables, 'Las tablas deben eliminarse tras uninstall');
    }

    /**
     * @depends testModuleCanUninstall
     */
    public function testConfigCleanedAfterUninstall(): void
    {
        $this->assertFalse(
            Configuration::get('ECOM_MIMODULO_ENABLED'),
            'Las configuraciones deben eliminarse tras uninstall'
        );
    }
}

#CI con GitHub Actions

.github/workflows/tests.yml
yaml
name: Module Tests
on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['7.4', '8.0', '8.1', '8.2']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, intl, gd, pdo_mysql
          coverage: xdebug
      - run: composer install --no-interaction
      - name: Unit Tests
        run: UNIT_ONLY=1 vendor/bin/phpunit --testsuite Unit
      - name: PHP CS Fixer
        run: vendor/bin/php-cs-fixer fix --dry-run --diff

  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: prestashop_test
        ports: ['3306:3306']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
      - name: Install PrestaShop
        run: |
          git clone --depth 1 --branch 8.1.x https://github.com/PrestaShop/PrestaShop.git /tmp/ps
          cd /tmp/ps && composer install --no-interaction
          php install-dev/index_cli.php \
            --db_server=127.0.0.1 --db_name=prestashop_test \
            --db_user=root --db_password=root \
            --prefix=ps_ --domain=localhost
      - name: Link module
        run: ln -s $GITHUB_WORKSPACE /tmp/ps/modules/ecom_mimodulo
      - name: Integration Tests
        run: PS_ROOT=/tmp/ps vendor/bin/phpunit --testsuite Integration
Descargar en Markdown Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.