codeception/module-amqp
Codeception module for testing AMQP messaging, usually RabbitMQ, in PHP. Adds helpers to declare queues and exchanges, publish messages, and grab or consume them in tests so you can verify message flows alongside your application code.
Install the module alongside Codeception itself; it pulls php-amqplib/php-amqplib and works over plain sockets, so no ext-amqp is required:
composer require --dev codeception/module-amqp
Enable it on the suite that exercises code which publishes or consumes messages — usually your functional or a dedicated integration suite, not unit:
# tests/functional.suite.yml
modules:
enabled:
- AMQP:
host: '%AMQP_HOST%'
port: 5672
username: '%AMQP_USER%'
password: '%AMQP_PASS%'
vhost: '/'
queues: ['orders.created', 'orders.failed']
cleanup: true
Read the source first, not the docs — with only a handful of stars there are few blog posts, but src/Codeception/Module/AMQP.php is a single class and short. Pay attention to three things: _initialize() (connection setup and queue declaration), _cleanup() (post-test purging, gated by cleanup), and the public see* / grab* / purge* methods, which are the entire API surface.
First day-one use case: your app has just handled a request/command that should have published an event. Assert it did, without standing up a real consumer:
$I->amOnPage('/checkout');
$I->seeResponseCodeIs(200);
$I->seeQueueSizeIs('orders.created', 1);
$I->seeMessageInQueue('orders.created', ['order_id' => 42]);
That single test replaces a lot of ad-hoc rabbitmqadmin scripting in CI.
Publisher assertions after an HTTP call. This is the bread-and-butter pattern: run the request through your framework module (Laravel, Symfony, Phalcon, etc.), then let AMQP inspect the broker. Because the two modules are independent, ordering matters — put the assertion after the request step, and remember the message may not arrive synchronously.
Seeding a queue before a consumer test. The inverse direction is just as useful. Push a fixture onto the queue with the module, then invoke your consumer class directly in-process (not as a long-running worker) and assert on the domain effect:
$I->purgeQueue('orders.created');
$I->haveMessageInQueue('orders.created', ['order_id' => 42]);
$consumer = new OrderCreatedConsumer($container->get('handler'));
$consumer->consume();
// then assert on DB state / mailer / etc.
Isolate with per-suite queues and a dedicated vhost. Never point this module at a vhost your dev or staging app also writes to. Prefer a test vhost or a test_ prefix injected via %AMQP_QUEUE_PREFIX%, and list every queue you touch in queues: so cleanup: true can purge them.
Lean on _cleanup rather than hand-rolled teardown. If you leave cleanup: true, the module purges configured queues between tests. That is usually enough. Only add explicit $I->purgeQueue(...) calls inside _before() when you have queues the config cannot know about, or when you want a guaranteed empty state at the start rather than the end.
Treat grabMessageFromQueue() as the consumer-side escape hatch. It returns a PhpAmqpLib\Message\AMQPMessage, which lets you assert on properties, content type, headers, and body bytes when the array-subset matcher isn't precise enough. Use it when you need to verify a serialization contract (e.g. that content_type is application/json).
Retry instead of sleep when the publisher is asynchronous. If messages can arrive after the response (queued listeners, deferred dispatch), wrap assertions with Codeception's retry support ($I->retrySeeMessageInQueue(...) with the Retry extension enabled) or a small polling helper in a Helper\Amqp trait. Avoid sleep().
Add a thin helper layer for your conventions. A Helper\Amqp that knows how to decode your envelope (e.g. extract payload from {meta, payload}) keeps test bodies readable and gives you one place to change if the message format evolves.
seeMessageInQueue() is a snapshot, not a wait. It reads the queue at call time. Asynchronous publishers will make this flaky unless you poll; treat a single call as valid only for synchronous publish paths.['order_id' => 42] generally succeeds if the body has more keys; passing a string usually compares the raw body. If your app publishes with an envelope, match against the envelope, not the inner payload.cleanup: true only purges queues named in queues:. Dynamically declared queues (e.g. per-tenant or per-test suffixes) will silently accumulate and poison later runs. Either enumerate them or purge explicitly.grabMessageFromQueue() is destructive. It consumes the message. Interleaving grab and dontSeeMessageInQueue will produce confusing results; assert first, grab last.grabMessageFromQueue() may get nothing. Don't run a worker in the same CI job as the assertions, or give each test its own queue._initialize(), so a missing RabbitMQ turns every test in the suite into an error, not a skip. Guard the suite in CI (health-check the container before running) or wrap the connection in a try/catch in your own module subclass.*.suite.yml. Use the %VAR% placeholder syntax shown above; Codeception resolves these from codeception.yml params or the environment.php-amqplib constraint against your PHP version; the pinned range is the most common install failure.haveMessageInQueue() helper, exchange assertions, or JSON-decoding matchers, subclass Codeception\Module\AMQP, add your methods, and enable the subclass in *.suite.yml — that's the supported path.--debug and tests/_output. Connection failures and PHP warnings are logged there; the module itself is quiet, so a failing seeQueue usually means a config typo (host, port, or vhost) rather than a missing queue.How can I help you explore Laravel packages today?