From 4c55de65cf325b3769f721442d206014007baa18 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves Date: Thu, 25 Jun 2026 13:39:43 -0500 Subject: [PATCH] Add Messenger assertions trait Add MessengerAssertionsTrait so functional tests can assert which messages were dispatched, how many, and on which bus. Like the Events/Mailer/Notifier traits, it reads Symfony's profiler "messenger" data collector. Steps: seeMessageDispatched, dontSeeMessageDispatched, seeDispatchedMessageCount and grabDispatchedMessageClasses. Per the module convention these use the see*/grab* idiom rather than assert*, which is reserved for direct ports of Symfony's own test assertions. Wire a dispatchable TestMessage into the test app (synchronous handler, no transport) and cover the steps in tests/MessengerAssertionsTest.php. symfony/messenger is a dev-only dependency; remove it before the Symfony 7.4 functional run, where the fixture app pulls in API Platform and would otherwise auto-register a Messenger processor bound to an undefined messenger.default_bus. --- .github/workflows/main.yml | 1 + composer.json | 1 + src/Codeception/Module/Symfony.php | 2 + .../Module/Symfony/DataCollectorName.php | 1 + .../Symfony/HttpKernelAssertionsTrait.php | 4 +- .../Symfony/MessengerAssertionsTrait.php | 134 ++++++++++++++++++ tests/MessengerAssertionsTest.php | 59 ++++++++ tests/_app/Controller/AppController.php | 9 ++ tests/_app/Message/TestMessage.php | 18 +++ .../MessageHandler/TestMessageHandler.php | 20 +++ tests/_app/TestKernel.php | 3 +- tests/_app/config/routes.php | 1 + tests/_app/config/services.php | 3 + 13 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 src/Codeception/Module/Symfony/MessengerAssertionsTrait.php create mode 100644 tests/MessengerAssertionsTest.php create mode 100644 tests/_app/Message/TestMessage.php create mode 100644 tests/_app/MessageHandler/TestMessageHandler.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf9dfe28..71725035 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,6 +54,7 @@ jobs: php vendor/bin/codecept run Functional -c framework-tests if [ "${{ matrix.symfony }}" = "7.4" ]; then + composer remove symfony/messenger --dev --no-interaction composer require codeception/module-rest --dev git -C framework-tests checkout -- composer.json git -C framework-tests apply resetFormatsAfterRequest_issue_test.patch diff --git a/composer.json b/composer.json index 3c051e63..2f0975ca 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ "symfony/http-foundation": "^5.4 | ^6.4 | ^7.4 | ^8.0", "symfony/http-kernel": "^5.4 | ^6.4 | ^7.4 | ^8.0", "symfony/mailer": "^5.4 | ^6.4 | ^7.4 | ^8.0", + "symfony/messenger": "^5.4 | ^6.4 | ^7.4 | ^8.0", "symfony/mime": "^5.4 | ^6.4 | ^7.4 | ^8.0", "symfony/notifier": "^5.4 | ^6.4 | ^7.4 | ^8.0", "symfony/options-resolver": "^5.4 | ^6.4 | ^7.4 | ^8.0", diff --git a/src/Codeception/Module/Symfony.php b/src/Codeception/Module/Symfony.php index b1684a87..c585d1a5 100644 --- a/src/Codeception/Module/Symfony.php +++ b/src/Codeception/Module/Symfony.php @@ -22,6 +22,7 @@ use Codeception\Module\Symfony\HttpKernelAssertionsTrait; use Codeception\Module\Symfony\LoggerAssertionsTrait; use Codeception\Module\Symfony\MailerAssertionsTrait; +use Codeception\Module\Symfony\MessengerAssertionsTrait; use Codeception\Module\Symfony\MimeAssertionsTrait; use Codeception\Module\Symfony\NotifierAssertionsTrait; use Codeception\Module\Symfony\ParameterAssertionsTrait; @@ -148,6 +149,7 @@ class Symfony extends Framework implements DoctrineProvider, PartedModule use HttpKernelAssertionsTrait; use LoggerAssertionsTrait; use MailerAssertionsTrait; + use MessengerAssertionsTrait; use MimeAssertionsTrait; use NotifierAssertionsTrait; use ParameterAssertionsTrait; diff --git a/src/Codeception/Module/Symfony/DataCollectorName.php b/src/Codeception/Module/Symfony/DataCollectorName.php index e6eb392a..e7cd5e27 100644 --- a/src/Codeception/Module/Symfony/DataCollectorName.php +++ b/src/Codeception/Module/Symfony/DataCollectorName.php @@ -18,5 +18,6 @@ enum DataCollectorName: string case TWIG = 'twig'; case SECURITY = 'security'; case MAILER = 'mailer'; + case MESSENGER = 'messenger'; case NOTIFIER = 'notifier'; } diff --git a/src/Codeception/Module/Symfony/HttpKernelAssertionsTrait.php b/src/Codeception/Module/Symfony/HttpKernelAssertionsTrait.php index 19094ad8..721b1a58 100644 --- a/src/Codeception/Module/Symfony/HttpKernelAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/HttpKernelAssertionsTrait.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\Mailer\DataCollector\MessageDataCollector; +use Symfony\Component\Messenger\DataCollector\MessengerDataCollector; use Symfony\Component\Notifier\DataCollector\NotificationDataCollector; use Symfony\Component\Translation\DataCollector\TranslationDataCollector; @@ -40,9 +41,10 @@ abstract protected function getProfile(): ?Profile; * ($name is DataCollectorName::TWIG ? TwigDataCollector : * ($name is DataCollectorName::SECURITY ? SecurityDataCollector : * ($name is DataCollectorName::MAILER ? MessageDataCollector : + * ($name is DataCollectorName::MESSENGER ? MessengerDataCollector : * ($name is DataCollectorName::NOTIFIER ? NotificationDataCollector : * DataCollectorInterface - * ))))))))) + * )))))))))) * ) */ protected function grabCollector(DataCollectorName $name, string $callingFunction = '', ?string $message = null): DataCollectorInterface diff --git a/src/Codeception/Module/Symfony/MessengerAssertionsTrait.php b/src/Codeception/Module/Symfony/MessengerAssertionsTrait.php new file mode 100644 index 00000000..70682cab --- /dev/null +++ b/src/Codeception/Module/Symfony/MessengerAssertionsTrait.php @@ -0,0 +1,134 @@ +dontSeeMessageDispatched(SendWelcomeEmail::class); + * $I->dontSeeMessageDispatched(SendWelcomeEmail::class, 'messenger.bus.default'); + * ``` + * + * @param class-string $messageClass + */ + public function dontSeeMessageDispatched(string $messageClass, ?string $bus = null): void + { + $this->assertNotContains( + $messageClass, + $this->getDispatchedMessageClasses(__FUNCTION__, $bus), + sprintf("The '%s' message was dispatched%s.", $messageClass, $this->busSuffix($bus)), + ); + } + + /** + * Returns the dispatched message class names, in dispatch order (optionally for a single bus). + * + * The profiler stores cloned snapshots, so this yields class names, not the message objects. + * + * ```php + * grabDispatchedMessageClasses(); + * $classes = $I->grabDispatchedMessageClasses('messenger.bus.default'); + * ``` + * + * @return list + */ + public function grabDispatchedMessageClasses(?string $bus = null): array + { + return $this->getDispatchedMessageClasses(__FUNCTION__, $bus); + } + + /** + * Asserts how many messages were dispatched (optionally on a single bus). + * + * ```php + * seeDispatchedMessageCount(1); + * $I->seeDispatchedMessageCount(2, 'messenger.bus.default'); + * ``` + */ + public function seeDispatchedMessageCount(int $expectedCount, ?string $bus = null): void + { + $messages = $this->grabMessengerCollector(__FUNCTION__)->getMessages($bus); + + $this->assertCount( + $expectedCount, + $messages, + sprintf( + 'Expected %d message(s) to be dispatched%s, but %d were.', + $expectedCount, + $this->busSuffix($bus), + count($messages), + ), + ); + } + + /** + * Asserts at least one message of the given class was dispatched (optionally on a single bus). + * + * ```php + * seeMessageDispatched(SendWelcomeEmail::class); + * $I->seeMessageDispatched(SendWelcomeEmail::class, 'messenger.bus.default'); + * ``` + * + * @param class-string $messageClass + */ + public function seeMessageDispatched(string $messageClass, ?string $bus = null): void + { + $this->assertContains( + $messageClass, + $this->getDispatchedMessageClasses(__FUNCTION__, $bus), + sprintf("The '%s' message was not dispatched%s.", $messageClass, $this->busSuffix($bus)), + ); + } + + /** + * @return list + */ + private function getDispatchedMessageClasses(string $callingFunction, ?string $bus): array + { + $classes = []; + foreach ($this->grabMessengerCollector($callingFunction)->getMessages($bus) as $entry) { + if (!$entry instanceof Data) { + continue; + } + + $message = $entry['message']; + $type = $message instanceof Data ? ($message['type'] ?? null) : null; + if ($type instanceof Data) { + $type = $type->getValue(); + } + + if (is_string($type) && class_exists($type)) { + $classes[] = $type; + } + } + + return $classes; + } + + private function busSuffix(?string $bus): string + { + return $bus !== null ? sprintf(" on bus '%s'", $bus) : ''; + } + + protected function grabMessengerCollector(string $callingFunction): MessengerDataCollector + { + return $this->grabCollector(DataCollectorName::MESSENGER, $callingFunction); + } +} diff --git a/tests/MessengerAssertionsTest.php b/tests/MessengerAssertionsTest.php new file mode 100644 index 00000000..05cabf40 --- /dev/null +++ b/tests/MessengerAssertionsTest.php @@ -0,0 +1,59 @@ +client->request('GET', '/dispatch-message'); + + $this->seeDispatchedMessageCount(1); + $this->seeDispatchedMessageCount(1, 'messenger.bus.default'); + $this->seeDispatchedMessageCount(0, 'non.existent.bus'); + } + + public function testSeeMessageDispatched(): void + { + $this->client->request('GET', '/dispatch-message'); + + $this->seeMessageDispatched(TestMessage::class); + $this->seeMessageDispatched(TestMessage::class, 'messenger.bus.default'); + } + + public function testDontSeeMessageDispatched(): void + { + $this->client->request('GET', '/dispatch-message'); + + $this->dontSeeMessageDispatched(stdClass::class); + $this->dontSeeMessageDispatched(TestMessage::class, 'non.existent.bus'); + } + + public function testGrabDispatchedMessageClasses(): void + { + $this->client->request('GET', '/dispatch-message'); + + $messages = $this->grabDispatchedMessageClasses(); + + $this->assertSame([TestMessage::class], $messages); + $this->assertSame([TestMessage::class], $this->grabDispatchedMessageClasses('messenger.bus.default')); + $this->assertSame([], $this->grabDispatchedMessageClasses('non.existent.bus')); + } + + public function testNoMessagesDispatched(): void + { + $this->client->request('GET', '/'); + + $this->seeDispatchedMessageCount(0); + $this->assertSame([], $this->grabDispatchedMessageClasses()); + } +} diff --git a/tests/_app/Controller/AppController.php b/tests/_app/Controller/AppController.php index 2a980b7c..9ff23287 100644 --- a/tests/_app/Controller/AppController.php +++ b/tests/_app/Controller/AppController.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Validator\Constraints\Email as EmailConstraint; use Symfony\Component\Validator\Constraints\NotBlank; @@ -23,6 +24,7 @@ use Tests\App\Event\TestEvent; use Tests\App\Mailer\MessageMailer; use Tests\App\Mailer\RegistrationMailer; +use Tests\App\Message\TestMessage; use Twig\Environment; final class AppController extends AbstractController @@ -66,6 +68,13 @@ public function dispatchOrphanEvent(EventDispatcherInterface $dispatcher): Respo return new Response('Orphan event dispatched'); } + public function dispatchTestMessage(MessageBusInterface $bus): Response + { + $bus->dispatch(new TestMessage('Hello from Messenger')); + + return new Response('Message dispatched'); + } + public function form(Request $request, FormFactoryInterface $formFactory, Environment $twig): Response { $builder = $formFactory->createNamedBuilder('registration_form', options: ['csrf_protection' => false]); diff --git a/tests/_app/Message/TestMessage.php b/tests/_app/Message/TestMessage.php new file mode 100644 index 00000000..3068b5a7 --- /dev/null +++ b/tests/_app/Message/TestMessage.php @@ -0,0 +1,18 @@ +content; + } +} diff --git a/tests/_app/MessageHandler/TestMessageHandler.php b/tests/_app/MessageHandler/TestMessageHandler.php new file mode 100644 index 00000000..9b48940a --- /dev/null +++ b/tests/_app/MessageHandler/TestMessageHandler.php @@ -0,0 +1,20 @@ + */ + public array $handled = []; + + public function __invoke(TestMessage $message): void + { + $this->handled[] = $message->getContent(); + } +} diff --git a/tests/_app/TestKernel.php b/tests/_app/TestKernel.php index 2dd0420b..d60eda30 100644 --- a/tests/_app/TestKernel.php +++ b/tests/_app/TestKernel.php @@ -54,12 +54,13 @@ private function configureExtensions(ContainerConfigurator $container): void 'profiler' => $profilerConfig, 'property_info' => ['enabled' => true], 'session' => ['handler_id' => null, 'storage_factory_id' => 'session.storage.factory.mock_file'], - 'mailer' => ['dsn' => 'null://null'], + 'mailer' => ['dsn' => 'null://null', 'message_bus' => false], 'default_locale' => 'en', 'translator' => ['default_path' => __DIR__ . '/translations', 'fallbacks' => ['es'], 'logging' => true], 'validation' => ['enabled' => true], 'form' => ['enabled' => true], 'notifier' => ['chatter_transports' => ['async' => 'null://null'], 'texter_transports' => ['sms' => 'null://null']], + 'messenger' => ['default_bus' => 'messenger.bus.default', 'buses' => ['messenger.bus.default' => []]], ]); $container->extension('twig', ['default_path' => __DIR__ . '/templates', 'debug' => true]); diff --git a/tests/_app/config/routes.php b/tests/_app/config/routes.php index 355a0c08..55fabb6f 100644 --- a/tests/_app/config/routes.php +++ b/tests/_app/config/routes.php @@ -11,6 +11,7 @@ $routes->add('dashboard', '/dashboard')->controller(AppController::class . '::dashboard'); $routes->add('deprecated', '/deprecated')->controller(AppController::class . '::deprecated'); $routes->add('dispatch_event', '/dispatch-event')->controller(AppController::class . '::dispatchEvent'); + $routes->add('dispatch_message', '/dispatch-message')->controller(AppController::class . '::dispatchTestMessage'); $routes->add('dispatch_named_event', '/dispatch-named-event')->controller(AppController::class . '::dispatchNamedEvent'); $routes->add('dispatch_orphan_event', '/dispatch-orphan-event')->controller(AppController::class . '::dispatchOrphanEvent'); $routes->add('form_handler', '/form')->controller(AppController::class . '::form'); diff --git a/tests/_app/config/services.php b/tests/_app/config/services.php index 5cae7a1a..0c5cf534 100644 --- a/tests/_app/config/services.php +++ b/tests/_app/config/services.php @@ -25,6 +25,7 @@ use Tests\App\Logger\ArrayLogger; use Tests\App\Mailer\MessageMailer; use Tests\App\Mailer\RegistrationMailer; +use Tests\App\MessageHandler\TestMessageHandler; use Tests\App\Notifier\NotifierFixture; use Tests\App\Repository\UserRepository; use Tests\App\Repository\UserRepositoryInterface; @@ -67,6 +68,8 @@ $services->set('notifier.notification_logger_listener', NotificationLoggerListener::class)->tag('kernel.event_subscriber'); $services->alias('notifier.logger_notification_listener', 'notifier.notification_logger_listener')->public(); + $services->set(TestMessageHandler::class); + $services->set(RegistrationMailer::class)->arg('$mailer', service('mailer')); $services->set(MessageMailer::class)->arg('$mailer', service('mailer')); $services->set(NotifierFixture::class)->arg('$dispatcher', service('event_dispatcher'));