Archives for February, 2019

Accessing Symfony private services in Behat

published on February 01, 2019.

Since Symfony 3.4 the services in the service container are private by default. While this decision made us write better production code by making us use Dependency Injection more and rely on the service container less, using these services in a test environment proved to be a challenge.

Since Symfony 4.1 there’s a special service container in the test environment which allows fetching private services in tests.

In a Behat test this test service container is not available through the static::$container property as it is in a WebTestCase or a KernelTestCase, but it is available under the test.service_container name in the service container.

We need Symfony, Behat, and Behat Symfony2 extension with the Behat Symfony2 extension configured to bootstrap an instance of the App\Kernel for us:

./behat.yml

default:
    extensions:
        Behat\Symfony2Extension:
          kernel:
            bootstrap: features/bootstrap/bootstrap.php
            class: App\Kernel

    suites:
      system:
        paths:
          - '%paths.base%/features/system.feature'
        contexts:
          - SystemContext:
              kernel: '@kernel'

If the behat.yml example looks weird, I’m reusing it from my previous blog post on testing Symfony commands with Behat.

Now that we have injected the kernel into our Context file, we can get the service container from the kernel, and from that service container access the test.service_container:

./features/bootstrap/SystemContext.php

<?php

use App\Kernel;
use Behat\Behat\Context\Context;

class SystemContext implements Context
{
    public function __construct(Kernel $kernel)
    {
        $testContainer = $kernel->getContainer()->get('test.service_container');
        $someService = $testContainer->get(App\Some\Service\We\Need::class);
    }

The test service container has all the services public and we can access them without worrying if they are private or public.

If you’re project is still on Symfony 3.4 or Symfony 4.0, Tomas Votruba has a blog post explaining how to achieve something similar using a compiler pass.

Happy hackin’!

Versions used for examples: Symfony 4.1, Behat 3.4, Behat Symfony2 Extension 2.1.
Tags: symfony, behat, private services, testing, php.
Categories: Programming, Development.

Testing Symfony commands with Behat

published on January 18, 2019.

The other day I was creating a Symfony command that will be periodically executed by a cronjob. I decided to write a Behat test for it, to see what a test like that would look like. Plus, just because it is executed by the system from a command line, doesn’t mean we can skimp on the business requirements.

We need Symfony, Behat, and Behat Symfony2 extension. In the behat.yml file we configure the Behat extension to boot up the Kernel for us and pass it in is a constructor argument to our Behat Context:

./behat.yml

default:
    extensions:
        Behat\Symfony2Extension:
          kernel:
            bootstrap: features/bootstrap/bootstrap.php
            class: App\Kernel

    suites:
      system:
        paths:
          - '%paths.base%/features/system.feature'
        contexts:
          - SystemContext:
              kernel: '@kernel'

We enable and configure the Behat Symfony2 extension, and tell Behat that the system suite will use the system.feature feature file and the SystemContext context which takes one constructor argument, the kernel. I don’t like to put everything into the default FeatureContext for Behat, but rather split different contexts into, well, different contexts. That’s why I created the separate SystemContext.

The boostrap.php file is created when installing the extension (at least, it was created for me as I installed it using Symfony Flex):

./features/bootstrap/bootstrap.php

<?php
putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = 'test');
require dirname(__DIR__, 2).'/config/bootstrap.php';

The system.feature file doesn’t have much, just an example scenario:

./features/system.feature

Feature: System executed commands

  Scenario: Behat testing a Symfony command
    Given I am the system
    When I greet the world
    Then I should say "Hello World"

The SystemContext

The SystemContext file is where it gets interesting:

./features/bootstrap/SystemContext.php

<?php

use App\Kernel;
use Behat\Behat\Context\Context;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Webmozart\Assert\Assert;

class SystemContext implements Context
{
    /**
     * @var Application
     */
    private $application;

    /**
     * @var BufferedOutput
     */
    private $output;

    public function __construct(Kernel $kernel)
    {
        $this->application = new Application($kernel);
        $this->output = new BufferedOutput();
    }

It implements the Behat Context interface so that Behat recognizes it as a context.

In the constructor we create a new console Application with the Kernel that the Behat Symfony2 extension created for us. We will use this application instance to run the command that we are testing. For all intents and purposes, this application instance acts the same as the application instance that gets created in the bin/console script that we usually use to run Symfony commands.

We also create a BufferedOutput in the constructor, that will hold the output that the command produces, which we can later on use to assert did the command produce the desired output.

Behat step definitions

The steps are defined like so (it’s in the same SystemContext.php file as the previous example):

./features/bootstrap/SystemContext.php

<?php

class SystemContext implements Context
{
    /**
     * @Given I am the system
     */
    public function iAmTheSystem()
    {
        Assert::same('cli', php_sapi_name());
    }

    /**
     * @When I greet the world
     */
    public function iGreetTheWorld()
    {
        $command = 'app:hello-world';
        $input = new ArgvInput(['behat-test', $command, '--env=test']);
        $this->application->doRun($input, $this->output);
    }

    /**
     * @Then I should say :sentence
     */
    public function iShouldSay($sentence)
    {
        $output = $this->output->fetch();

        Assert::same($sentence, $output);
    }

I am the system

In the first Behat step we assert that the PHP interface is the cli. Not sure how it could be anything else in this case, but let’s have that in there.

I greet the world

The second Behat step is where the fun part happens, where we run the command. The ArgvInput takes an array of parameters from the CLI in the argv format. In the case of bin/console it ends up being populated from $_SERVER['argv']. In this case though, we need to populate it on our own.

The first argument is always the name that was used to run the script and it ends up being just a “placeholder”, hence the behat-test value. We can put in anything there, really.

The second parameter is the command that we want to run: app:hello-world. It is the same string we would use when executing that command through bin/console. Because we created an instance of the Application in the constructor, Symfony will know exactly what command that is.

The third parameter is an option to tell Symfony to run the command in the test environment.

Once we have the input ready, we tell the application to run using the doRun method, passing in the input and the output (which is a BufferedOutput).

I should say :sentence

In the third Behat step we fetch the output and assert that it is the same as the output we expected it to be.

Make it reusable

To make it a bit more reusable, the running of the command in the iGreetTheWorld step can be extracted to a private method so that it all reads a little bit nicer. The final result looks something like this:

./features/bootstrap/SystemContext.php

<?php

use App\Kernel;
use Behat\Behat\Context\Context;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Webmozart\Assert\Assert;

class SystemContext implements Context
{
    /**
     * @var Application
     */
    private $application;

    /**
     * @var BufferedOutput
     */
    private $output;

    public function __construct(Kernel $kernel)
    {
        $this->application = new Application($kernel);
        $this->output = new BufferedOutput();
    }

    /**
     * @Given I am the system
     */
    public function iAmTheSystem()
    {
        Assert::same('cli', php_sapi_name());
    }

    /**
     * @When I greet the world
     */
    public function iGreetTheWorld()
    {
        $this->runCommand('app:hello-world');
    }

    /**
     * @Then I should say :sentence
     */
    public function iShouldSay($sentence)
    {
        $output = $this->output->fetch();

        Assert::same($sentence, $output);
    }

    private function runCommand(string $command)
    {
        $input = new ArgvInput(['behat-test', $command, '--env=test']);
        $this->application->doRun($input, $this->output);
    }

Happy hackin’!

Versions used for examples: Symfony 4.1, Behat 3.4, Behat Symfony2 Extension 2.1.
Tags: symfony, behat, commands, bdd, testing, php.
Categories: Programming, Development.

A lookback on 2018

published on January 01, 2019.

2018 was a good year. A year of changes.

My wife and I are expecting a baby in March 2019. We bought a house. We traveled. We drank some lovely tea. We read books. We ate great food in good company. We made new friends in a new city.

I changed jobs. I spoke at four conferences, one uncon, and two meetups. I wrote a two part article on Mockery for phparch. I gave a talk at the new job how to better use test doubles. I became a Dungeon Master for my Dungeons & Dragons group.

Looking forward to 2019

I want to be home as much as possible with my wife and our kid, so I’ll continue working from home. I’m gonna dial it back on the conferences, so my only plan for this year is to visit the PHP Serbia conference in May. But the learning can’t stop, the plan is to redirect the conference budget to online courses and materials.

In 2019 I want to learn how not to create total shite User Experiences and User Interfaces on the web. Bought the Refactoring UI material as a first step. Started learning what makes a modern web app in this day and age. I’ll keep learning about Domain-Driven Design and do my best to implement what I’ve learned so far. Keep blogging about it all.

I want to pick up woodworking as a hobby. Together with the house we bought a bunch of tools from the previous owners, so now it’s just the matter of going down to the garage and practicing. I also want to create a herb garden.

Here’s to another amazing year.

Happy hackin’!

Tags: 2018, a good year, new house, new job.
Categories: Blablabla.
Robert Basic

Robert Basic

Software engineer, consultant, open source contributor.

Let's work together!

If you require outsourcing or consulting help on your projects, I'm available!

Robert Basic © 2008 — 2019
Get the feed