Skip to content

PHP Unit Testing

PHP Unit Testing

Overview

This guide will cover the creation of PHPUnit tests in Shopware 6. Refer to the official PHPUnit documentation for a deep dive into PHP unit testing.

Prerequisites

In order to create tests for a plugin, you need a plugin as a base. Refer to the Plugin Base Guide for more information.

Furthermore, have a look at our Execute database queries/migrations guide since this guide will show you how to create a migration test for these examples.

PHPUnit configuration

First, to configure PHPUnit, create a file called phpunit.xml in the root directory of the plugin. To get more familiar with the configurable options, refer to the PHPUnit documentation. This example explains configuring PHPUnit to search in the directories <plugin root>/src/Test and <plugin root>/src/Migration/Test for your tests.

The phpunit.xml can be autogenerated for you with the bin/console plugin:create command:

xml
// <plugin root>/phpunit.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.3/phpunit.xsd"
         bootstrap="tests/TestBootstrap.php"
         executionOrder="random">
    <coverage>
        <include>
            <directory>./src/</directory>
        </include>
    </coverage>
    <php>
        <ini name="error_reporting" value="-1"/>
        <server name="KERNEL_CLASS" value="Shopware\Core\Kernel"/>
        <env name="APP_ENV" value="test"/>
        <env name="APP_DEBUG" value="1"/>
        <env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
    </php>
    <testsuites>
        <testsuite name="migration">
            <directory>Migration/Test</directory>
        </testsuite>
    
        <testsuite name="Example Testsuite">
            <directory>Test</directory>
        </testsuite>
    </testsuites>
</phpunit>

This command will also generate a TestBootstrap.php file:

php
// <plugin root>/tests/TestBootstrap.php
<?php declare(strict_types=1);

use Shopware\Core\TestBootstrapper;

$loader = (new TestBootstrapper())
    ->addCallingPlugin()
    ->addActivePlugins('BasicExample')
    ->setForceInstallPlugins(true)
    ->bootstrap()
    ->getClassLoader();

$loader->addPsr4('Swag\\BasicExample\\Tests\\', __DIR__);

The setForceInstallPlugins method ensures that your plugin is installed and active even the test database was already build beforehand.

Example Tests

Integration test

After PHPUnit is configured, a first test can be written. In this example, a test simply tries to instantiate every .php class to see if any used core classes are missing. In the test, you use the IntegrationTestBehaviour trait, which comes with some handy features, such as automatically setting up a database transaction or clearing the cache before starting tests. This is how your test could look like:

php
// <plugin root>/src/Test/UsedClassesAvailableTest.php
<?php declare(strict_types=1);

namespace Swag\BasicExample\Test;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour;
use Symfony\Component\Finder\Finder;

class UsedClassesAvailableTest extends TestCase
{
    use IntegrationTestBehaviour;

    public function testClassesAreInstantiable(): void
    {
        $namespace = str_replace('\Test', '', __NAMESPACE__);

        foreach ($this->getPluginClasses() as $class) {
            $classRelativePath = str_replace(['.php', '/'], ['', '\\'], $class->getRelativePathname());

            $this->getMockBuilder($namespace . '\\' . $classRelativePath)
                ->disableOriginalConstructor()
                ->getMock();
        }

        // Nothing broke so far, classes seem to be instantiable
        $this->assertTrue(true);
    }

    private function getPluginClasses(): Finder
    {
        $finder = new Finder();
        $finder->in(realpath(__DIR__ . '/../'));
        $finder->exclude('Test');
        return $finder->files()->name('*.php');
    }
}

Migration test

In order to test the example migration Migration1611740369ExampleDescription, create a new test called Migration1611740369ExampleDescriptionTest, which extends from the PHPUnit TestCase. Use the KernelTestBehaviour trait because a database connection from the container is needed.

This is an example for a migration test:

php
// <plugin root>/src/Migration/Test/Migration1611740369ExampleDescriptionTest.php
<?php declare(strict_types=1);

namespace Swag\BasicExample\Migration\Test;

use Doctrine\DBAL\Connection;
use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Test\TestCaseBase\KernelTestBehaviour;

class Migration1611740369ExampleDescriptionTest extends TestCase
{
    use KernelTestBehaviour;

    public function testNoChanges(): void
    {
        /** @var Connection $conn */
        $conn = $this->getContainer()->get(Connection::class);
        $expectedSchema = $conn->fetchAssoc('SHOW CREATE TABLE `swag_basic_example_general_settings`')['Create Table'];

        $migration = new Migration1611740369ExampleDescription();

        $migration->update($conn);
        $actualSchema = $conn->fetchAssoc('SHOW CREATE TABLE `swag_basic_example_general_settings`')['Create Table'];
        static::assertSame($expectedSchema, $actualSchema, 'Schema changed!. Run init again to have clean state');

        $migration->updateDestructive($conn);
        $actualSchema = $conn->fetchAssoc('SHOW CREATE TABLE `swag_basic_example_general_settings`')['Create Table'];
        static::assertSame($expectedSchema, $actualSchema, 'Schema changed!. Run init again to have clean state');
    }

    public function testNoTable(): void
    {
        /** @var Connection $conn */
        $conn = $this->getContainer()->get(Connection::class);
        $conn->executeStatement('DROP TABLE `swag_basic_example_general_settings`');

        $migration = new Migration1611740369ExampleDescription();
        $migration->update($conn);
        $exists = $conn->fetchColumn('SELECT COUNT(*) FROM `swag_basic_example_general_settings`') !== false;

        static::assertTrue($exists);
    }
}

Mocking services

In some cases a service should behave differently in a test run. Such a case could be where a service deletes a file or makes a critical api call. To avoid this in a test run it is possible to create a <plugin root>/Resources/config/services_test.{xml|yml} file which will override your <plugin root>/Resources/config/services.{xml|yml}. But only for the test environment.

In this test-only service config you can override arguments, aliases or parameters to change what the service container injects into services during a test run.

Executing the test

To execute tests, a PHPUnit binary is necessary, which is most likely located in the vendor/bin folder. The command below will use the phpunit.xml file in the custom/plugins/SwagBasicExample folder and execute the testsuite with the name migration.

sh
// <project root>ell
./vendor/bin/phpunit --configuration="custom/plugins/SwagBasicExample" --testsuite "migration"

Executing all tests in the plugin

If no testsuite is passed, it will execute all testsuites.

shell
./vendor/bin/phpunit --configuration="custom/plugins/SwagBasicExample"

Executing a single class or method

To execute a specific test class or method of a testsuite, pass the argument --filter with the name of the class or method.

shell
./vendor/bin/phpunit --configuration="custom/plugins/SwagBasicExample" --filter testNoChanges
./vendor/bin/phpunit --configuration="custom/plugins/SwagBasicExample" --filter Migration1611740369ExampleDescriptionTest

Flex template

In order to run PHPunit tests install the flex template dev-tools package via composer.

shell
composer require --dev dev-tools

Next steps

Running unit tests with javascript code is explained in the following two articles: