Asserting the output of Symfony console commands
If you write console commands using symfony/console
and have not used SymfonyStyle
yet, you should give it a try.
SymfonyStyle
provides additional methods for rendering output in the terminal. These methods allow rendering titles, section headings, success and error messages, tables, bullet lists, and more - all of them nicely formatted and indented.
Here is an example from ergebnis/day-one-to-obsidian-converter
:
<?php
declare(strict_types=1);
namespace Ergebnis\DayOneToObsidianConverter\Outside\Adapter\Primary\Console;
use Ergebnis\DayOneToObsidianConverter\Inside;
use Symfony\Component\Console;
final class ConvertCommand extends Console\Command\Command
{
protected function execute(
Console\Input\InputInterface $input,
Console\Output\OutputInterface $output,
): int {
$io = new Console\Style\SymfonyStyle(
$input,
$output,
);
// ...
$dayOneDirectory = $input->getArgument('day-one-directory');
if (!\is_dir($dayOneDirectory)) {
$io->error(\sprintf(
'DayOne directory %s does not exist',
$dayOneDirectory,
));
return self::FAILURE;
}
// ...
return self::SUCCESS;
}
}
While the error message looks great, when you write tests and want to make assertions about the output generated by the command, you quickly run into issues.
If the length of an error or success message depends on variable input, you can never be sure whether a message can fit into a single or will spread across multiple lines.
<?php
namespace Ergebnis\DayOneToObsidianConverter\Test\Integration\Outside\Adapter\Primary\Console;
use Ergebnis\DayOneToObsidianConverter\Outside;
use Ergebnis\DayOneToObsidianConverter\Test;
use PHPUnit\Framework;
use Symfony\Component\Console;
final class ConvertCommandTest extends Framework\TestCase
{
use Test\Util\Helper;
public function testExecuteFailsWhenDayOneDirectoryDoesNotExist(): void
{
// ...
$application = self::createApplication();
$input = new Console\Input\ArrayInput([
'day-one-directory' => $dayOneDirectory,
]);
$output = new Console\Output\BufferedOutput();
$exitCode = $application->run(
$input,
$output,
);
self::assertSame(Console\Command\Command::FAILURE, $exitCode);
$expected = \sprintf(
'DayOne directory %s does not exist',
$dayOneDirectory,
);
self::assertStringContainsString($expected, $output->fetch());
}
// ...
}
Unfortunately, the test above fails because the actual error message is wrapped across multiple lines.
While Symfony allows specifying the terminal width via a COLUMNS
environment variable, SymfonyStyle
does not accept values greater than 120 characters. Setting the COLUMNS
environment variable to a value larger than 120 in the context of tests will not work.
You could try working around the formatting and indenting by fiddling with regular expressions, but why bother?
Instead, let SymfonyStyle
generate the expected output!
<?php
declare(strict_types=1);
namespace Ergebnis\DayOneToObsidianConverter\Test\Util;
use Faker\Factory;
use Faker\Generator;
use Symfony\Component\Console;
trait Helper
{
// ...
/**
* @param \Closure(OutputStyle):void $closure
*/
final protected static function captureConsoleOutput(\Closure $closure): string
{
$output = new Console\Output\BufferedOutput();
$io = new Console\Style\SymfonyStyle(
new Console\Input\ArrayInput([]),
$output,
);
$closure($io);
return $output->fetch();
}
// ...
}
Now you can simplify your tests by asserting that the actual output contains the expected output.
self::assertSame(Console\Command\Command::FAILURE, $exitCode);
- $expected = \sprintf(
- 'DayOne directory %s does not exist',
- $dayOneDirectory,
- );
+ $expected = self::captureConsoleOutput(static function (Console\Style\OutputStyle $io) use ($dayOneDirectory): void {
+ $io->error(\sprintf(
+ 'DayOne directory %s does not exist',
+ $dayOneDirectory,
+ ));
+ });
self::assertStringContainsString($expected, $output->fetch());
}
// ...
}
Do you have a better idea?
Do you find this article helpful?
Do you have feedback?
Just blogged: Asserting the output of @symfony console commands.https://t.co/COjucFS85n
— Andreas Möller (@localheinz) August 29, 2022