Skip to content

Instantly share code, notes, and snippets.

@tPl0ch
Last active August 18, 2023 11:18
  • Star 29 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save tPl0ch/6706427 to your computer and use it in GitHub Desktop.
A CommandContext for Behat tests of Symfony Console Commands
Feature: Command Demo Feature
In order to show how to describe commands in Behat
As a Behat developer
I need to show simple scenario based on http://symfony.com/doc/2.0/components/console.html#testing-commands
Scenario: Test command
When I run "demo:command" command
Then I should see "/.../"
Then the command exit code should be 0
Scenario: Test command with parameters
When I run "demo:command" with parameters:
"""
{
"argument": "cool",
"option": "some_option"
}
"""
Then I should see "/.../"
Scenario: Test failing command
When I run "demo:failing:command"
Then the command exception "CommandException" with message "Expected Exception message" should be thrown
Then the command exit code should be 24
<?php
namespace MFB\Behat\Subcontext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\MinkExtension\Context\RawMinkContext;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Class CommandContext
*/
class CommandContext extends RawMinkContext
{
/**
* @var string
*/
private $consoleAppClass;
/**
* @var \Symfony\Component\Console\Application
*/
private $application;
/**
* @var array
*/
private $registeredCommands;
/**
* @var array
*/
private $loadedCommands;
/**
* @var \Symfony\Component\Console\Tester\CommandTester
*/
private $tester;
/**
* @var \Exception
*/
private $commandException;
/**
* @var array
*/
private $commandParameters;
/**
* @var string
*/
private $runCommand;
/**
* @var array
*/
private $listeners;
/**
* @var int
*/
private $exitCode;
/**
* @param string $consoleAppClass
* @param array $registeredCommands
* @param array $listeners
*
* @throws \InvalidArgumentException
*/
public function __construct($consoleAppClass, array $registeredCommands = array(), array $listeners = array())
{
$this->checkClassIsLoaded($consoleAppClass);
$this->consoleAppClass = $consoleAppClass;
$this->registeredCommands = $registeredCommands;
$this->loadedCommands = array();
$this->listeners = $listeners;
$this->commandParameters = array();
$this->exitCode = 0;
$this->useContext('exception', new ExceptionContext());
}
/**
* @Given /^I run a command "([^"]*)"$/
*/
public function iRunACommand($command)
{
$commandInstance = $this->getCommand($command);
$this->tester = new CommandTester($commandInstance);
try {
$this->exitCode = $this
->tester
->execute(
$this->getCommandParams($command)
)
;
$this->commandException = null;
} catch (\Exception $exception) {
$this->commandException = $exception;
$this->exitCode = $exception->getCode();
}
$this->runCommand = $command;
$this->commandParameters = array();
}
/**
* @Given /^I run a command "([^"]*)" with parameters:$/
*/
public function iRunACommandWithParameters($command, PyStringNode $parameterJson)
{
$this->commandParameters = json_decode($parameterJson->getRaw(), true);
if (null === $this->commandParameters) {
throw new \InvalidArgumentException(
"PyStringNode could not be converted to json."
);
}
$this->iRunACommand($command);
}
/**
* @Then /^The command exception "([^"]*)" should be thrown$/
*/
public function theCommandExceptionShouldBeThrown($exceptionClass)
{
$this->checkThatCommandHasRun();
$this
->getSubcontext('exception')
->setException($this->commandException)
->assertException($exceptionClass)
;
}
/**
* @Then /^The command exit code should be (\d+)$/
*/
public function theCommandExitCodeShouldBe($exitCode)
{
$this->checkThatCommandHasRun();
assertEquals($exitCode, $this->exitCode);
}
/**
* @Then /^I should see "([^"]*)" in the command output$/
*/
public function iShouldSeeInTheCommandOutput($regexp)
{
$this->checkThatCommandHasRun();
assertRegExp($regexp, $this->tester->getDisplay());
}
/**
* @Then /^The command exception "([^"]*)" with message "([^"]*)" should be thrown$/
*/
public function theCommandExceptionWithMessageShouldBeThrown($exceptionClass, $exceptionMessage)
{
$this->checkThatCommandHasRun();
$this
->getSubcontext('exception')
->setException($this->commandException)
->assertException($exceptionClass)
;
$this
->getSubcontext('exception')
->assertExceptionMessage($exceptionMessage)
;
}
/**
* @return \Symfony\Component\Console\Application
*
* @throws \LogicException
*/
public function getApplication()
{
if (null !== $this->application) {
return $this->application;
}
$callingContext = $this->getMainContext();
if (!$callingContext instanceof KernelAwareInterface) {
throw new \LogicException(
"CommandContext or MainContext must implement 'KernelAwareInterface'"
);
}
$kernel = $callingContext->getKernel();
if (null === $kernel) {
throw new \LogicException(
"The kernel hasn't been initialized yet. Please use this method only in a step method."
);
}
$this->application = new $this->consoleAppClass($kernel);
$this->processConsoleEventListeners();
return $this->application;
}
/**
* @param string $command
*
* @return \Symfony\Component\Console\Command\Command
*
* @throws \InvalidArgumentException
*/
public function getCommand($command)
{
$command = (string) $command;
if ($this->isLoaded($command)) {
return $this->loadedCommands[$command];
}
if (!$this->isRegistered($command)) {
throw new \InvalidArgumentException(
sprintf('Command with name "%s" is not registered with the Context', $command)
);
}
$commandInstance = new $this->registeredCommands[$command]();
$application = $this->getApplication();
$application->add($commandInstance);
return $this->loadedCommands[$command] = $commandInstance;
}
/**
* @param string $commandName
* @param string $commandClass
*/
public function registerCommand($commandName, $commandClass)
{
$this->checkClassIsLoaded($commandClass);
$this->registeredCommands[(string) $commandName] = $commandClass;
}
/**
* @param string $commandName
*
* @return bool
*/
public function unregisterCommand($commandName)
{
$commandName = (string) $commandName;
if (!isset($this->registeredCommands[$commandName])) {
return false;
}
unset($this->registeredCommands[$commandName]);
return true;
}
/**
* @param string $command
*
* @return bool
*/
public function isRegistered($command)
{
return (isset($this->registeredCommands[(string) $command]));
}
/**
* @param string $command
*
* @return bool
*/
public function isLoaded($command)
{
return (isset($this->loadedCommands[(string) $command]));
}
/**
* @param string $class
*
* @throws \InvalidArgumentException
*/
private function checkClassIsLoaded($class)
{
if (!class_exists((string) $class)) {
throw new \InvalidArgumentException(
'Class "%s" could not be found or autoloaded.'
);
}
}
/**
* @return bool
*
* @throws \LogicException
*/
private function checkThatCommandHasRun()
{
if (null === $this->runCommand) {
throw new \LogicException(
"You first need to run a command to check to use this step"
);
}
return true;
}
/**
* Processes the subscribers and listeners for this Application
*/
private function processConsoleEventListeners()
{
if (null === $this->application) {
return null;
}
if (!empty($this->listeners)) {
$dispatcher = new EventDispatcher();
$listeners = array_merge(
array(
'subscriber' => array(),
'listener' => array()
),
$this->listeners
);
foreach ($listeners['listener'] as $event => $listener) {
$priority = 0;
if (is_array($listeners)) {
list($listener, $priority) = $listener;
}
$dispatcher->addListener($event, $listener, $priority);
}
foreach ($listeners['subscriber'] as $subscriber) {
$dispatcher->addSubscriber($subscriber);
}
$this
->application
->setDispatcher($dispatcher)
;
}
}
/**
* @param string $command
*
* @return array
*/
private function getCommandParams($command)
{
$default = array(
'command' => $command
);
return array_merge(
$this->commandParameters,
$default
);
}
}
<?php
namespace Application\Bundle\ApiBundle\Features\Context;
use Application\Bundle\ApiBundle\Listener\ConsoleEventSubscriber;
use MFB\Behat\Context\KernelAwareFeatureContext;
use MFB\Behat\Subcontext\CommandContext;
use MFB\Behat\Subcontext\ResponseContext;
use VIPSoft\DoctrineDataFixturesExtension\Context\FixtureAwareContextInterface;
/**
* Feature context.
*/
class FeatureContext extends KernelAwareFeatureContext implements FixtureAwareContextInterface
{
/**
* This method should return an array of FQCN DataFixture class names.
*
* @return array
*/
public function getFixtures()
{
return array(
'MFB\DataFixturesBundle\DataFixtures\ORM\SomeFixture',
'MFB\DataFixturesBundle\DataFixtures\ORM\AnotherFixture'
);
}
/**
* @BeforeScenario
*/
public function beforeScenario()
{
$this
->getSession()
->setRequestHeader('HTTP_ACCEPT', 'application/json,text/html')
;
}
/**
* @param array $parameters
*/
public function __construct($parameters)
{
$this->useContext('response', new ResponseContext($parameters));
$this->useContext(
'command',
new CommandContext(
'Symfony\Bundle\FrameworkBundle\Console\Application',
array(
'mfb:test:exception:command' => 'Application\Bundle\ApiBundle\Tests\Stub\ExceptionThrowingCommand',
'mfb:test:failing:command' => 'Application\Bundle\ApiBundle\Tests\Stub\FailingCommand'
),
array(
'subscriber' => array(
new ConsoleEventSubscriber()
)
)
));
}
}
@mloureiro
Copy link

This looks really interesting.

Got 2 questions:
Q1: How do you handle Exceptions?

Q2: And since this uses CommandTester it's possible to test commands that change the system (ex: cache:clear)?

@kendoctor
Copy link

how can i behat interactive command ? especially for many asks and askConfirmations .

@bagonyi
Copy link

bagonyi commented Nov 11, 2014

To test interactive commands you need to create a fake DialogHelper and inject it to the command: http://marekkalnik.tumblr.com/post/32601882836/symfony2-testing-interactive-console-command

@Apostropher
Copy link

Very helpful.

@jeremyjumeau
Copy link

I found KNP approach interesting and I test my sf command like that:

class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
    private $output;
    /**
     * @When I run :command
     */
    public function iRun($command)
    {
        $this->output = shell_exec("php bin/console " . $command);
    }

    /**
     * @Then I should see :string in the output
     */
    public function iShouldSeeInTheOutput($string)
    {
        if (strpos($this->output, $string) === false) {
            throw new \Exception(sprintf('Did not see "%s" in output "%s"', $string, $this->output));
        }
    }
}

@OskarStark
Copy link

Nice, but IMO you don't need to extend MinkContext @jeremyjumeau

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment