Skip to content

[Console] Allow to test the different streams at the same time #61494

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: 7.4
Choose a base branch
from

Conversation

theofidry
Copy link
Contributor

@theofidry theofidry commented Aug 21, 2025

Q A
Branch? 7.4
Bug fix? no
New feature? yes
Deprecations? yes
Issues Fix #...
License MIT

Currently testing a command looks like this:

// tests/Command/CreateUserCommandTest.php
namespace App\Tests\Command;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class CreateUserCommandTest extends KernelTestCase
{
    private CommandTester $commandTester;

    protected function setup(): void
    {
        self::bootKernel();

        $application = new Application(self::$kernel);

        $command = $application->find('app:create-user');
        $this->commandTester = new CommandTester($command);
    }

    public function testExecute(): void
    {
        $this->commandTester->execute(['username' => 'Wouter']);

        $commandTester->assertCommandIsSuccessful();

        $output = $commandTester->getDisplay();
        $this->assertStringContainsString('Username: Wouter', $output);
    }
}

However, sometimes it is important to test not just the combined output of the
command, but the stdout and stderr streams separately. One can do so using the
capture_stderr_separately option:

// tests/Command/CreateUserCommandTest.php
namespace App\Tests\Command;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class CreateUserCommandTest extends KernelTestCase
{
    private CommandTester $commandTester;

    protected function setup(): void
    {
        self::bootKernel();

        $application = new Application(self::$kernel);

        $command = $application->find('app:create-user');
        $this->commandTester = new CommandTester($command);
    }

    public function testExecute(): void
    {
        $this->commandTester->execute(
            ['username' => 'Wouter'],
            ['capture_stderr_separately' => true],
        );

        $commandTester->assertCommandIsSuccessful();

        $stdoutOutput = $commandTester->getDisplay();
        $stderrOutput = $commandTester->getErrorOutput();
    }
}

It works well. However, you know are testing two separate outputs. If the output
is important to you and you want to make sure things are displayed correctly to
the user, not just in each individual streams, you need to have both tests.

It is unnecessarily tedious. Instead, this PR proposes to expose and output that
offers both options at the same time.

// tests/Command/CreateUserCommandTest.php
namespace App\Tests\Command;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class CreateUserCommandTest extends KernelTestCase
{
    private CommandTester $commandTester;

    protected function setup(): void
    {
        self::bootKernel();

        $application = new Application(self::$kernel);

        $command = $application->find('app:create-user');
        $this->commandTester = new CommandTester($command);
    }

    public function testExecute(): void
    {
        $result = $this->commandTester->run(
            ['username' => 'Wouter'],
        );

        $result->assertIsSuccessful();

        $combinedOutput = $result->getDisplay();

        $stdoutOutput = $result->getOutput();
        $stderrOutput = $result->getErrorOutput();
    }
}

This is the general spirit of this MR. Unfortunately I'm easily distracted, so I picked up a few things on the way which make sense as a whole but maybe should be left out for now.

  • Review the general direction
  • Update the changelog
  • Update the upgrade

@theofidry theofidry requested a review from chalasr as a code owner August 21, 2025 18:55
@carsonbot carsonbot added this to the 7.4 milestone Aug 21, 2025
@carsonbot carsonbot changed the title Feat/test combined output Feat/test combined output Aug 21, 2025
@@ -74,6 +76,7 @@ protected function doWrite(string $message, bool $newline): void
fflush($this->stream);
}

// TODO: should be public & static?
/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this piece of code is implemented and use in several places. I wonder if it would make sense to either expose it as a static public utility or extract it and re-use that extracted utility here.

But irrelevant to the MR, to clean up

public function __construct(
callable|Command $command,
callable|Command $command,
// TODO: to discuss if we want it in the constructor with the setters or only in the ::run() method
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open question

];
}

// TODO: to discuss if we want fluent setters
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open question (note that if we have them in the constructor instead, we do not need those setters).

In general I am not a fan of fluent setters, but in the context of such test utilities it does make it a bit nicer. It's very subjective though

@@ -49,29 +123,90 @@ public function __construct(
*/
public function execute(array $input, array $options = []): int
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to deprecate in favour of the ::run() IMO

}

// TODO: here we need to pass the result first if we want to keep some arguments optional.
// Not wanting this is an argument for having the assertions in the ExecutionResult instead.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to keep the approach of having traits in the test cases, then I think this trait makes sense. Otherwise it would make more sense to have this as part of ExecutionResult IMO

/**
* Collection of output normalizers
*/
final class OutputNormalizers
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strictly speaking not needed for the purpose of the MR so could be scrapped. I otherwise thought it would be interesting to have a dedicated re-usable utility rather than private methods left and right.

@theofidry theofidry changed the title Feat/test combined output [Console] Allow to test the different streams at the same time Aug 21, 2025
@theofidry
Copy link
Contributor Author

The application tester should also be adapted

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

Successfully merging this pull request may close these issues.

2 participants