Skip to content
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

Added FixtureTrait and ConsoleTrait, refactored console command tests to use Application tester. #267

Merged
merged 3 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@
remove_php_command() {
rm -Rf php-command || true
rm -Rf src || true
rm -Rf tests/phpunit/Unit/Command || true
rm -Rf tests/phpunit/Functional/ApplicationFunctionalTestCase.php || true
rm -Rf tests/phpunit/Functional/JokeCommandTest.php || true
rm -Rf tests/phpunit/Functional/SayHelloCommandTest.php || true

Check warning on line 170 in init.sh

View check run for this annotation

Codecov / codecov/patch

init.sh#L168-L170

Added lines #L168 - L170 were not covered by tests
rm -f docs/content/php/php-command.mdx || true

remove_tokens_with_content "PHP_COMMAND"
Expand Down
23 changes: 23 additions & 0 deletions tests/phpunit/Functional/ApplicationFunctionalTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace YourNamespace\App\Tests\Functional;

use PHPUnit\Framework\TestCase;
use YourNamespace\App\Tests\Traits\ArrayTrait;
use YourNamespace\App\Tests\Traits\AssertTrait;
use YourNamespace\App\Tests\Traits\MockTrait;

/**
* Class ApplicationFunctionalTestCase.
*
* Base class to unit test scripts.
*/
abstract class ApplicationFunctionalTestCase extends TestCase {

use ArrayTrait;
use AssertTrait;
use MockTrait;

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

declare(strict_types=1);

namespace YourNamespace\App\Tests\Unit\Command;
namespace YourNamespace\App\Tests\Functional;

use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\Console\Command\Command;
use YourNamespace\App\Command\JokeCommand;
use YourNamespace\App\Tests\Traits\ConsoleTrait;
use YourNamespace\App\Tests\Traits\MockTrait;

/**
* Class JokeCommandTest.
Expand All @@ -19,32 +20,33 @@
#[CoversMethod(JokeCommand::class, 'configure')]
#[CoversMethod(JokeCommand::class, 'getJoke')]
#[Group('command')]
class JokeCommandTest extends CommandTestCase {
class JokeCommandTest extends ApplicationFunctionalTestCase {

use ConsoleTrait;
use MockTrait;

#[DataProvider('dataProviderExecute')]
public function testExecute(string $content, int $expected_code, array|string $expected_output = []): void {
public function testExecute(string $content, array $expected_output, bool $expected_fail = FALSE): void {
/** @var \YourNamespace\App\Command\JokeCommand $mock */
// @phpstan-ignore varTag.nativeType
$mock = $this->prepareMock(JokeCommand::class, [
'getContent' => $content,
]);
$mock->setName('joke');

$output = $this->runExecute($mock);

$this->assertEquals($expected_code, $this->commandTester->getStatusCode());
$expected_output = is_array($expected_output) ? $expected_output : [$expected_output];
$this->consoleInitApplicationTester($mock);
$output = $this->consoleApplicationRun([], [], $expected_fail);
foreach ($expected_output as $expected_output_string) {
$this->assertArrayContainsString($expected_output_string, $output);
$this->assertStringContainsString($expected_output_string, $output);
}
}

public static function dataProviderExecute(): array {
return [
[static::fixturePayload(['setup' => 'Test setup', 'punchline' => 'Test punchline']), Command::SUCCESS, ['Test setup', 'Test punchline']],
['', Command::FAILURE, ['Unable to retrieve a joke.']],
['non-json', Command::FAILURE, ['Unable to retrieve a joke.']],
[static::fixturePayload(['setup' => 'Test setup']), Command::FAILURE, ['Unable to retrieve a joke.']],
[static::fixturePayload(['setup' => 'Test setup', 'punchline' => 'Test punchline']), ['Test setup', 'Test punchline']],
['', ['Unable to retrieve a joke.'], TRUE],
['non-json', ['Unable to retrieve a joke.'], TRUE],
[static::fixturePayload(['setup' => 'Test setup']), ['Unable to retrieve a joke.'], TRUE],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

declare(strict_types=1);

namespace YourNamespace\App\Tests\Unit\Command;
namespace YourNamespace\App\Tests\Functional;

use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\Group;
use YourNamespace\App\Command\SayHelloCommand;
use YourNamespace\App\Tests\Traits\ConsoleTrait;

/**
* Class SayHelloCommandTest.
Expand All @@ -16,11 +17,15 @@
#[CoversMethod(SayHelloCommand::class, 'execute')]
#[CoversMethod(SayHelloCommand::class, 'configure')]
#[Group('command')]
class SayHelloCommandTest extends CommandTestCase {
class SayHelloCommandTest extends ApplicationFunctionalTestCase {

use ConsoleTrait;

public function testExecute(): void {
$output = $this->runExecute(SayHelloCommand::class);
$this->assertArrayContainsString('Hello, Symfony console!', $output);
$this->consoleInitApplicationTester(SayHelloCommand::class);

$output = $this->consoleApplicationRun();
$this->assertStringContainsString('Hello, Symfony console!', $output);
}

}
109 changes: 109 additions & 0 deletions tests/phpunit/Traits/ConsoleTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace YourNamespace\App\Tests\Traits;

use PHPUnit\Framework\AssertionFailedError;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\ApplicationTester;

/**
* Trait ConsoleTrait.
*
* Helpers to work with Console.
*
* @phpstan-ignore trait.unused
*/
trait ConsoleTrait {

use ReflectionTrait;

/**
* Application tester.
*/
protected ApplicationTester $appTester;

/**
* Initialize application tester.
*
* @param string|object $object_or_class
* Command class or object.
* @param bool $is_single_command
* Is single command. Defaults to TRUE.
*/
protected function consoleInitApplicationTester(string|object $object_or_class, bool $is_single_command = TRUE): void {
$application = new Application();

$instance = is_object($object_or_class) ? $object_or_class : new $object_or_class();
if (!$instance instanceof Command) {
throw new \InvalidArgumentException('The provided object is not an instance of Command');
}

$application->add($instance);

$name = $instance->getName();
if (empty($name)) {
$ret = $this->getProtectedValue($instance, 'defaultName');
if (!empty($ret) || !is_string($ret)) {
throw new \InvalidArgumentException('The provided object does not have a valid name');
}
$name = $ret;
}

$application->setDefaultCommand($name, $is_single_command);

$application->setAutoExit(FALSE);
$application->setCatchExceptions(FALSE);
$application->setCatchErrors(FALSE);

$this->appTester = new ApplicationTester($application);
}

/**
* Run console application.
*
* @param array<string, string> $input
* Input arguments.
* @param array<string, string> $options
* Options.
* @param bool $expect_fail
* Whether a failure is expected. Defaults to FALSE.
*
* @return string
* Run output (stdout or stderr).
*/
protected function consoleApplicationRun(array $input = [], array $options = [], bool $expect_fail = FALSE): string {
$output = '';

$options += ['capture_stderr_separately' => TRUE];

try {
$this->appTester->run($input, $options);
$output = $this->appTester->getDisplay();

if ($this->appTester->getStatusCode() !== 0) {
throw new \Exception(sprintf("Application exited with non-zero code.\nThe output was:\n%s\nThe error output was:\n%s", $this->appTester->getDisplay(), $this->appTester->getErrorOutput()));
}

if ($expect_fail) {
throw new AssertionFailedError(sprintf("Application exited successfully but should not.\nThe output was:\n%s\nThe error output was:\n%s", $this->appTester->getDisplay(), $this->appTester->getErrorOutput()));
}
}
catch (\RuntimeException $exception) {
if (!$expect_fail) {
throw new AssertionFailedError('Application exited with an error:' . PHP_EOL . $exception->getMessage());
}
$output = $exception->getMessage();
}
catch (\Exception $exception) {
if (!$expect_fail) {
throw new AssertionFailedError('Application exited with an error:' . PHP_EOL . $exception->getMessage());
}
}

return $output;
}

}
82 changes: 82 additions & 0 deletions tests/phpunit/Traits/FixtureTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace YourNamespace\App\Tests\Traits;

use Symfony\Component\Filesystem\Filesystem;

/**
* Trait FixtureTrait.
*
* Helpers to work with fixture files.
*
* @phpstan-ignore trait.unused
*/
trait FixtureTrait {

/**
* Fixture directory.
*/
protected string $fixtureDir;

/**
* Initialize fixture directory.
*
* @param string|null $name
* Optional fixture name.
* @param string|null $root
* Optional root directory.
*/
public function fixtureInit(?string $name, ?string $root = NULL): void {
$name = $name ?? get_class($this);
$root = $root ?? sys_get_temp_dir();
$this->fixtureDir = $root . DIRECTORY_SEPARATOR . date('U') . DIRECTORY_SEPARATOR . $name;
}

/**
* Create fixture file at provided path.
*
* @param string $path
* File path.
* @param string $name
* Optional file name.
* @param string|array<string> $content
* Optional file content.
*
* @return string
* Created file name.
*/
protected function fixtureCreateFile(string $path, string $name = '', string|array $content = ''): string {
$fs = new Filesystem();

$name = $name !== '' && $name !== '0' ? $name : 'tmp' . rand(1000, 100000);
$path = $path . DIRECTORY_SEPARATOR . $name;

$dir = dirname($path);
if (!empty($dir)) {
$fs->mkdir($dir);
}

$fs->touch($path);
if (!empty($content)) {
$content = is_array($content) ? implode(PHP_EOL, $content) : $content;
$fs->dumpFile($path, $content);
}

return $path;
}

/**
* Remove fixture file at provided path.
*
* @param string $path
* File path.
* @param string $name
* File name.
*/
protected function fixtureRemoveFile(string $path, string $name): void {
(new Filesystem())->remove($path . DIRECTORY_SEPARATOR . $name);
}

}
65 changes: 0 additions & 65 deletions tests/phpunit/Unit/Command/CommandTestCase.php

This file was deleted.

Loading
Loading