Now We Need to Write our First PHPUnit Tests

We need to have at least one test in our project in order to run QA

We will start with some basic "Smoke Test" level testing which add a useful safety net and sanity check to our project.

HTTP Smoke Testing

This is a smoke testing framework that will automatically test all configured routes

For the smoke testing system, we need to edit composer.json directly

1
2
3
4
5
6
7
8
9
    "require-dev": {
      "shopsys/http-smoke-testing": "dev-master@dev"
    },
    "repositories":[
        {
            "type": "vcs",
            "url": "git@github.com:edmondscommerce/http-smoke-testing.git"
        }
    ]

Writing First Tests

We need at least one unit test and get it passing

Before we can do that, we need to handle basic configurations

Now Write Our First Test

We will start with a "Service Container Smoke Test" that simply loads every service

Abstract Service Test

First of all, we need an abstract container test case

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php declare(strict_types=1);

namespace MyProject\Tests\Assets;

use MyProject\Helper\EnvironmentHelper;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Yaml\Yaml;

/**
 * This abstract test uses the real symfony container but in a test friendly mode that allows all services to be
 * retrieved even if they are private
 *
 * @see https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing
 */
abstract class AbstractServiceTest extends KernelTestCase
{
    protected function setup(): void
    {
        $this->ensureServicesTestExistsAndIsValid();
    }

    /**
     * We need to ensure that services are public
     * We also need to ensure that the services_test.yml file is up to date with the main services.yml file
     */
    private function ensureServicesTestExistsAndIsValid(): void
    {
        $servicesPath     = __DIR__ . '/../../config/services.yaml';
        $servicesTestPath = __DIR__ . '/../../config/services_test.yaml';
        self::assertFileExists($servicesPath);
        self::assertFileExists($servicesTestPath);
        $servicesContents     = \ts\file_get_contents($servicesPath);
        $servicesTestContents = \ts\file_get_contents($servicesTestPath);
        $servicesYaml         = Yaml::parse($servicesContents);
        $servicesTestYaml     = Yaml::parse($servicesTestContents);
        self::assertTrue($servicesTestYaml['services']['_defaults']['public']);
        $servicesTestYamlWithoutPublic                                    = $servicesTestYaml;
        $servicesTestYamlWithoutPublic['services']['_defaults']['public'] = false;
        self::assertSame(
            $servicesYaml,
            $servicesTestYamlWithoutPublic,
            'Your services_test does not seem to be up to date with your services.yml'
        );

    }

    protected function getProjectDir(): string
    {
        return $this->getContainer()->get(EnvironmentHelper::class)->getProjectDir();
    }

    /**
     * Use this method to load your services.
     *
     * You will need to explicitly type hint your service as a class property
     *
     * WARNING - if you are testing a service that is not injected in any other class, it will have been removed from
     * the container and you will not be able to load it
     *
     * @return ContainerInterface
     */
    protected function getContainer(): ContainerInterface
    {
        if (null === static::$container) {
            static::bootKernel();
        }

        return static::$container;
    }
}

Service Smoke Test

The service smoke test simply ensures we can fire up services without exceptions being thrown

We explicitly remove it from code coverage as it doesn't really test anything as such.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php declare(strict_types=1);

namespace MyProject\Tests\Large;

use MyProject\Tests\Assets\AbstractServiceTest;

/**
 * @large
 * @coversNothing
 */
final class AllServiceSmokeTest extends AbstractServiceTest
{
    /**
     * @test
     * @dataProvider provideService
     *
     * @param string $service
     */
    public function itCanLoadAllServices(string $service): void
    {
        $object = $this->getContainer()->get($service);
        self::assertNotNull($object);
    }

    /**
     * @return array<string,array<string>>
     */
    public function provideService(): array
    {
        $this->setupForDataProviders();
        $container = $this->getContainer();
        $data      = [];
        foreach ($container->getServiceIds() as $service) {
            $service        = (string)$service;
            $data[$service] = [$service];

        }

        return $data;
    }
}

Controller Smoke Test

Like the all services test, this is a large test and it simply kicks the tyres on each route.

It will automatically test new routes as they are added to configurations.

Routes are likely to require a little configuration to get the test working

TODO: write more docs on the controller smoke test somewhere else

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php declare(strict_types=1);

namespace ProjectName\Tests\Large;

use ProjectName\Controller\IndexController;
use Shopsys\HttpSmokeTesting\HttpSmokeTestCase;
use Shopsys\HttpSmokeTesting\RouteConfigCustomizer;

/**
 * @large
 * @coversNothing - smoke tests should not contribute towards coverage
 */
final class AllControllersSmokeTest extends HttpSmokeTestCase
{
    private const EXPECT_CODE_ROUTES = [
        IndexController::ROUTE_INDEX_NAME => 302,
    ];

    /**
     * This method must be implemented to customize and configure the test cases for individual routes
     *
     * @param RouteConfigCustomizer $routeConfigCustomizer
     */
    protected function customizeRouteConfigs(RouteConfigCustomizer $routeConfigCustomizer): void
    {
        foreach (self::EXPECT_CODE_ROUTES as $routeName => $expectedCode) {
            $this->setExpectedCodeForRoute($routeConfigCustomizer, $routeName, $expectedCode);
        }
    }
}