Skip to content

Commit

Permalink
Initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
frankdekker committed Jun 23, 2023
1 parent 57049d8 commit 953b5a0
Show file tree
Hide file tree
Showing 27 changed files with 1,063 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['8.0', '8.1', '8.2']
php-versions: ['8.1', '8.2']
composer-flags: ['', '--prefer-lowest']
steps:
- uses: actions/checkout@v3
Expand Down
58 changes: 46 additions & 12 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
{
"name": "digitalrevolution/skeleton",
"description": "Digital Revolution skeleton package",
"name": "digitalrevolution/phpunit-extensions",
"description": "A library for phpunit utility and support classes",
"type": "library",
"license": "MIT",
"minimum-stability": "stable",
"config": {
"sort-packages": true,
"allow-plugins": {
"phpstan/extension-installer": true
"phpstan/extension-installer": true,
"digitalrevolution/php-codesniffer-baseline": true
}
},
"require": {
"php": ">=8.0"
"php": "^8.1",
"phpunit/phpunit": "^9.5.24 || ^10.0"
},
"require-dev": {
"digitalrevolution/phpunit-file-coverage-inspection": "^v2.0.0",
"digitalrevolution/phpunit-file-coverage-inspection": "^2.0",
"phpmd/phpmd": "^2.12",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.9.1",
"phpstan/phpstan-phpunit": "^1.2.2",
"phpstan/phpstan-strict-rules": "^1.5",
"phpstan/phpstan-symfony": "^1.2.16",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.6",
"phpmd/phpmd": "@stable",
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.1",
"phpstan/extension-installer": "^1.1"
"squizlabs/php_codesniffer": "^3.7",
"symfony/form": "^6.0",
"symfony/framework-bundle": "^6.0",
"symfony/security-core": "^6.0",
"symfony/twig-bundle": "^6.0",
"symfony/validator": "^6.0"
},
"scripts": {
"check": ["@check:phpstan", "@check:phpmd", "@check:phpcs"],
Expand All @@ -34,5 +41,32 @@
"test": "phpunit",
"test:integration": "phpunit --testsuite integration",
"test:unit": "phpunit --testsuite unit"
},
"suggest": {
"symfony/form": "Symfony form component is required for testing the controller createForm methods",
"symfony/framework-bundle": "Symfony framework bundle is required for the AbstractControllerTestCase",
"symfony/security-core": "Symfony security component is required for testing authentication/authorization related methods",
"symfony/twig-bundle": "Symfony twig bundle is required to test rendering templates/forms"
},
"conflict": {
"symfony/form": "<6.0",
"symfony/framework-bundle": "<6.0",
"symfony/security-core": "<6.0",
"symfony/twig-bundle": "<6.0"
},
"autoload": {
"files": [
"src/Mock/consecutive.php"
],
"psr-4": {
"DR\\PHPUnitExtensions\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"DR\\PHPUnitExtensions\\Tests\\": "tests/",
"DR\\PHPUnitExtensions\\Tests\\Integration\\": "tests/Integration/",
"DR\\PHPUnitExtensions\\Tests\\Unit\\": "tests/Unit/"
}
}
}
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ parameters:
treatPhpDocTypesAsCertain: false
paths:
- src
- tests
5 changes: 4 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
executionOrder="defects"
>
<testsuites>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
Expand Down
Empty file removed src/.gitkeep
Empty file.
50 changes: 50 additions & 0 deletions src/Mock/ConsecutiveParameters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace DR\PHPUnitExtensions\Mock;

use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\Constraint\IsEqual;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\ExpectationFailedException;

/**
* @internal
*/
class ConsecutiveParameters
{
private int $callIndex = 0;

/**
* @param array<int, mixed|Constraint> $expectedArguments
*
* @throws Exception
*/
public function __construct(private readonly array $expectedArguments)
{
}

/**
* @throws ExpectationFailedException
*/
public function evaluate(mixed $actualArgument): bool
{
if (array_key_exists($this->callIndex, $this->expectedArguments) === false) {
throw new ExpectationFailedException(
sprintf('consecutive was called %d times, but only received arguments for %d', $this->callIndex + 1, count($this->expectedArguments))
);
}

// take the argument for the correct call index
$expectedArgument = $this->expectedArguments[$this->callIndex];

// evaluate the argument against the expected argument
$constraint = $expectedArgument instanceof Constraint ? $expectedArgument : new IsEqual($expectedArgument);
$constraint->evaluate($actualArgument);

++$this->callIndex;

return true;
}
}
45 changes: 45 additions & 0 deletions src/Mock/consecutive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace DR\PHPUnitExtensions\Mock;

use InvalidArgumentException;
use PHPUnit\Framework\Constraint\Callback;

/**
* @param array<mixed> $firstInvocationArguments A list of arguments for each invocation that will be asserted against.
* Will fail on: too many invocations or if the argument doesn't match for each invocation.
* @param array<mixed> $secondInvocationArguments
* @param array<mixed> ...$expectedArgumentList
*
* @note Full qualified name to appease to phpstorm inspection gods
* phpcs:ignore
* @return \PHPUnit\Framework\Constraint\Callback<mixed>[]
* @example <code>->with(...consecutive([5, 'foo'], [6, 'bar']))</code>
*/
function consecutive(array $firstInvocationArguments, array $secondInvocationArguments, array ...$expectedArgumentList): array
{
array_unshift($expectedArgumentList, $secondInvocationArguments);
array_unshift($expectedArgumentList, $firstInvocationArguments);

// reorganize arguments per argument index
$argumentsByIndex = [];
foreach ($expectedArgumentList as $invocation => $expectedArguments) {
if (count($expectedArguments) === 0) {
throw new InvalidArgumentException('consecutive() is expecting at least 1 or more arguments for invocation #' . ((int)$invocation + 1));
}

foreach ($expectedArguments as $index => $argument) {
$argumentsByIndex[$index][] = $argument;
}
}

$callbacks = [];
foreach ($argumentsByIndex as $arguments) {
$constraint = new ConsecutiveParameters($arguments);
$callbacks[] = new Callback(static fn($actualArgument): bool => $constraint->evaluate($actualArgument));
}

return $callbacks;
}
121 changes: 121 additions & 0 deletions src/Symfony/AbstractConstraintValidatorTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace DR\PHPUnitExtensions\Symfony;

use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormFactoryBuilder;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilder;

/**
* @template V of ConstraintValidator
* @template C of Constraint
*/
abstract class AbstractConstraintValidatorTestCase extends TestCase
{
protected const IGNORE_INVALID_VALUE = 'UT_IGNORE_INVALID_VALUE';

/** @var V */
protected ConstraintValidator $validator;

/** @var C */
protected Constraint $constraint;

protected ExecutionContextInterface&MockObject $executionContext;
protected ConstraintViolationBuilder&MockObject $violationBuilder;
protected Form $form;

/**
* @return V
*/
abstract protected function getValidator(): ConstraintValidator;

/**
* @return C
*/
abstract protected function getConstraint(): Constraint;

/**
* @throws Exception
*/
protected function setUp(): void
{
parent::setUp();
$this->violationBuilder = $this->createMock(ConstraintViolationBuilder::class);
$this->executionContext = $this->createMock(ExecutionContextInterface::class);

$this->validator = $this->getValidator();
$this->constraint = $this->getConstraint();

$this->validator->initialize($this->executionContext);
}

protected function initRootForm(): Form
{
$formBuilder = new FormBuilder('foobar', null, new EventDispatcher(), (new FormFactoryBuilder())->getFormFactory());
$formBuilder->setCompound(true);
$formBuilder->setDataMapper(new DataMapper());
$this->form = new Form($formBuilder);

return $this->form;
}

/**
* @throws Exception
*/
protected function assertHandlesIncorrectConstraintType(mixed $value = null): void
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate($value, $this->createMock(Constraint::class));
}

protected function expectNoViolations(): void
{
$this->executionContext->expects(static::never())->method('buildViolation');
$this->executionContext->expects(static::never())->method('addViolation');
}

/**
* Expect a violation to be added using addViolation method.
* e.g. $this->context->addViolation($constraint->message);
*
* @param array<int|string, mixed> $parameters
*/
protected function expectViolation(string $message, array $parameters = []): void
{
$this->executionContext->expects(static::once())->method('addViolation')->with($message, $parameters);
}

/**
* Expect a violation to be created using the violation builder.
* e.g. $this->context->buildViolation($constraint->message)->atPath('price')->setInvalidValue($price)->addViolation();
*
* @param array<int|string, mixed> $parameters
*/
protected function expectViolationViaBuilder(
string $message,
array $parameters = [],
?string $atPath = null,
mixed $invalidValue = self::IGNORE_INVALID_VALUE
): void {
$this->executionContext->expects(static::once())->method('buildViolation')->with($message, $parameters)->willReturn($this->violationBuilder);
if ($atPath !== null) {
$this->violationBuilder->expects(static::once())->method('atPath')->with($atPath)->willReturnSelf();
}
if ($invalidValue !== self::IGNORE_INVALID_VALUE) {
$this->violationBuilder->expects(static::once())->method('setInvalidValue')->with($invalidValue)->willReturnSelf();
}
$this->violationBuilder->expects(static::once())->method('addViolation');
}
}
Loading

0 comments on commit 953b5a0

Please sign in to comment.