I have a newly created Symfony 7 app and I have a DTO called: AuthCallbackDto
<?php
declare(strict_types=1);
namespace AppApplicationModelAuth;
use SymfonyComponentValidatorConstraints as Assert;
readonly class AuthCallbackDto
{
public function __construct(
#[AssertNotBlank(message: 'The `code` value cannot be blank')]
#[AssertLength(min: 5)]
private string $code,
#[AssertNotBlank(message: 'The `session_state` value cannot be blank')]
#[AssertLength(min: 5)]
private string $session_state,
) {
}
public function getCode(): string
{
return $this->code;
}
public function getSessionState(): string
{
return $this->session_state;
}
}
I use this in a controller like so:
<?php
declare(strict_types=1);
namespace AppApplicationControllerAuth;
use AppApplicationModelAuthAuthCallbackDto;
use SymfonyComponentHttpFoundationJsonResponse;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelAttributeAsController;
use SymfonyComponentHttpKernelAttributeMapQueryString;
use SymfonyComponentRoutingAttributeRoute;
#[AsController]
class Callback
{
#[Route('/auth/callback', name: 'auth/callback', methods: ['GET'])]
public function test(
#[MapQueryString] AuthCallbackDto $authCallbackDto,
): Response {
return new JsonResponse([
'code' => $authCallbackDto->getCode(),
'session_state' => $authCallbackDto->getSessionState()
]);
}
}
If the code or session_state values are not present or < 5 characters then an error is thrown. This is good!
http://localhost:8000/auth/callback?code=oihiohoih&session_state=34f34f34f – Works (good)
http://localhost:8000/auth/callback?code=&session_state= – fails (good)
‘http://localhost:8000/auth/callback?code=123&session_state=123 – fails (good)
Feeling over confident, I then decided to write a very small unit test for the DTO class. However, I then noticed that the constraint attributes are not being applied when I run PHP Unit from the console. The exception in the test is never thrown.
Can anyone tell me what I am doing wrong or am I simply assuming too much?
Here is my unit test:
<?php
declare(strict_types=1);
namespace AppTestsUnitApplicationModelAuh;
use AppApplicationModelAuthAuthCallbackDto;
use PHPUnitFrameworkAttributesDataProvider;
use PHPUnitFrameworkTestCase;
class AuhCallbackDtoTest extends TestCase
{
/**
* @return array<array<int, string|bool>>
*/
public static function dtoData(): array
{
return [
[
'',
'',
true,
],
[
'someCodeABC12344',
'someSessionState',
false,
],
];
}
#[DataProvider('dtoData')]
public function testDtoCanBeCreated(string $code, string $sessionState, bool $exception): void
{
if ($exception) {
$this->expectException(Exception::class);
}
$dto = new AuthCallbackDto($code, $sessionState);
self::assertEquals($dto->getCode(), $code);
self::assertEquals($dto->getSessionState(), $sessionState);
}
}
I have tried debugging the validation rules using console and looks good:
bin/console debug:validator 'AppApplicationModelAuthAuthCallbackDto'
AppApplicationModelAuthAuthCallbackDto
------------------------------------------
+---------------+--------------------------------------------------+--------------------------+---------------------------------------------------------------------------------+
| Property | Name | Groups | Options |
+---------------+--------------------------------------------------+--------------------------+---------------------------------------------------------------------------------+
| code | property options | | [ |
| | | | "cascadeStrategy" => |
| | | | "None", |
| | | | "autoMappingStrategy" => |
| | | | "None", |
| | | | "traversalStrategy" => |
| | | | "None" |
| | | | ] |
| code | SymfonyComponentValidatorConstraintsNotBlank | Default, AuthCallbackDto | [ |
| | | | "allowNull" => |
| | | | false, |
| | | | "message" => "The `code` |
| | | | value cannot be blank", |
| | | | "normalizer" => |
| | | | null, |
| | | | "payload" => |
| | | | null |
| | | | ] |
| code | SymfonyComponentValidatorConstraintsLength | Default, AuthCallbackDto | [ |
| | | | "charset" => |
| | | | "UTF-8", |
| | | | "charsetMessage" => "This |
| | | | value does not match the expected {{ charset }} charset.", |
| | | | "countUnit" => |
| | | | "codepoints", |
| | | | "exactMessage" => "This |
| | | | value should have exactly {{ limit }} character.|This value should have exactly |
| | | | {{ limit }} characters.", |
| | | | "max" => |
| | | | null, |
| | | | "maxMessage" => "This value |
| | | | is too long. It should have {{ limit }} character or less.|This value is too |
| | | | long. It should have {{ limit }} characters or less.", |
| | | | "min" => 5, |
| | | | "minMessage" => "This value |
| | | | is too short. It should have {{ limit }} character or more.|This value is too |
| | | | short. It should have {{ limit }} characters or more.", |
| | | | "normalizer" => |
| | | | null, |
| | | | "payload" => |
| | | | null |
| | | | ] |
| session_state | property options | | [ |
| | | | "cascadeStrategy" => |
| | | | "None", |
| | | | "autoMappingStrategy" => |
| | | | "None", |
| | | | "traversalStrategy" => |
| | | | "None" |
| | | | ] |
| session_state | SymfonyComponentValidatorConstraintsNotBlank | Default, AuthCallbackDto | [ |
| | | | "allowNull" => |
| | | | false, |
| | | | "message" => "The |
| | | | `session_state` value cannot be blank", |
| | | | "normalizer" => |
| | | | null, |
| | | | "payload" => |
| | | | null |
| | | | ] |
| session_state | SymfonyComponentValidatorConstraintsLength | Default, AuthCallbackDto | [ |
| | | | "charset" => |
| | | | "UTF-8", |
| | | | "charsetMessage" => "This |
| | | | value does not match the expected {{ charset }} charset.", |
| | | | "countUnit" => |
| | | | "codepoints", |
| | | | "exactMessage" => "This |
| | | | value should have exactly {{ limit }} character.|This value should have exactly |
| | | | {{ limit }} characters.", |
| | | | "max" => |
| | | | null, |
| | | | "maxMessage" => "This value |
| | | | is too long. It should have {{ limit }} character or less.|This value is too |
| | | | long. It should have {{ limit }} characters or less.", |
| | | | "min" => 5, |
| | | | "minMessage" => "This value |
| | | | is too short. It should have {{ limit }} character or more.|This value is too |
| | | | short. It should have {{ limit }} characters or more.", |
| | | | "normalizer" => |
| | | | null, |
| | | | "payload" => |
| | | | null |
| | | | ] |
+---------------+--------------------------------------------------+--------------------------+---------------------------------------------------------------------------------+
I can update the constructor of my DTO to have some standard Webmozart static assert calls but that will be unnecessary when the app runs as intended.
...
public function __construct(
#[AssertNotBlank]
#[AssertLength(min: 5)]
private string $code,
#[AssertNotBlank]
#[AssertLength(min: 5)]
private string $session_state,
) {
WebmozartAssertAssert::notEmpty($this->code);
WebmozartAssertAssert::minLength($code, 5);
WebmozartAssertAssert::notEmpty($this->session_state);
WebmozartAssertAssert::minLength($this->session_state, 5);
}
...
2
Answers
Thanks @Leroy, in the end I went with the WebTestCase approach:
Those
Assert
attributes comes from the Symfony Validator component. Php doesn’t automatically runs business logic on it. But it does do this in the framework.You need to pass this through the symfony validator.
However, since you are testing a Controller, you could create a
WebTestCase
instead. You can (functionally) test your controllers by sending a request and validating the response. In short, run functional tests as if your application is a "black box".See this chapter for more information: https://symfony.com/doc/current/testing.html#write-your-first-application-test
If you really not want this, and you really want to test your Dto. You have to validate your Dto using the Symfony Validator.
FYI, I haven’t really tested this. So the
Validation::createValidator()
may need some additional configuration to understand those constructor property attributes, but I’m not entirely sure.