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

Envelope collection improvements #6

Merged
merged 6 commits into from
Apr 28, 2021
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
with:
php-version: 7.4
coverage: none
tools: php-cs-fixer:2.18.3
tools: php-cs-fixer

- name: Check CS
run: php-cs-fixer fix --dry-run --diff --diff-format=udiff
67 changes: 41 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ class MyTest extends KernelTestCase // or WebTestCase
}
```

### Other Transport Assertions
### Other Transport Assertions and Helpers

```php
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Zenstruck\Messenger\Test\InteractsWithMessenger;

class MyTest extends KernelTestCase // or WebTestCase
Expand All @@ -109,31 +111,44 @@ class MyTest extends KernelTestCase // or WebTestCase
{
// ...some code that routes messages to your configured transport

// assert against the sent messages
$this->messenger()->sent()->assertEmpty();
$this->messenger()->sent()->assertNotEmpty();
$this->messenger()->sent()->assertCount(3);
$this->messenger()->sent()->assertContains(MyMessage::class); // contains this message
$this->messenger()->sent()->assertContains(MyMessage::class, 3); // contains this message 3 times
$this->messenger()->sent()->assertNotContains(MyMessage::class); // not contains this message

// assert against the acknowledged messages
// these are messages that were successfully processed
$this->messenger()->acknowledged()->assertEmpty();
$this->messenger()->acknowledged()->assertNotEmpty();
$this->messenger()->acknowledged()->assertCount(3);
$this->messenger()->acknowledged()->assertContains(MyMessage::class); // contains this message
$this->messenger()->acknowledged()->assertContains(MyMessage::class, 3); // contains this message 3 times
$this->messenger()->acknowledged()->assertNotContains(MyMessage::class); // not contains this message

// assert against the rejected messages
// these are messages were not successfully processed
$this->messenger()->rejected()->assertEmpty();
$this->messenger()->rejected()->assertNotEmpty();
$this->messenger()->rejected()->assertCount(3);
$this->messenger()->rejected()->assertContains(MyMessage::class); // contains this message
$this->messenger()->rejected()->assertContains(MyMessage::class, 3); // contains this message 3 times
$this->messenger()->rejected()->assertNotContains(MyMessage::class); // not contains this message
$queue = $this->messenger()->queued();
$sent = $this->messenger()->sent();
$acknowledged = $this->messenger()->acknowledged(); // messages successfully processed
$rejected = $this->messenger()->rejected(); // messages not successfully processed

// The 4 above variables are all instances of Zenstruck\Messenger\Test\EnvelopeCollection
// which is a countable iterator with the following api (using $queue for the example).
// Methods that return Envelope(s) actually return TestEnvelope(s) which is an Envelope
// decorator (all standard Envelope methods can be used) with some stamp-related assertions.

// collection assertions
$queue->assertEmpty();
$queue->assertNotEmpty();
$queue->assertCount(3);
$queue->assertContains(MyMessage::class); // contains this message
$queue->assertContains(MyMessage::class, 3); // contains this message 3 times
$queue->assertNotContains(MyMessage::class); // not contains this message

// helpers
$queue->count(); // number of envelopes
$queue->all(); // TestEnvelope[]
$queue->messages(); // object[] the messages unwrapped from their envelope
$queue->messages(MyMessage::class); // MyMessage[] just instances of the passed message class

// get specific envelope
$queue->first(); // TestEnvelope - first one on the collection
$queue->first(MyMessage::class); // TestEnvelope - first where message class is MyMessage
$queue->first(function(Envelope $e) {
return $e->getMessage() instanceof MyMessage && $e->getMessage()->isSomething();
}); // TestEnvelope - first that matches the filter callback

// Equivalent to above - use the message class as the filter function typehint to
// auto-filter to this message type.
$queue->first(fn(MyMessage $m) => $m->isSomething()); // TestEnvelope

// TestEnvelope stamp assertions
$queue->first()->assertHasStamp(DelayStamp::class);
$queue->first()->assertNotHasStamp(DelayStamp::class);
}
}
```
Expand Down
64 changes: 63 additions & 1 deletion src/EnvelopeCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,32 @@ public function assertNotContains(string $messageClass): self
return $this;
}

/**
* @param string|callable|null $filter
*/
public function first($filter = null): TestEnvelope
{
if (null === $filter) {
// just the first envelope
return $this->first(fn() => true);
}

if (!\is_callable($filter)) {
// first envelope for message class
return $this->first(fn(Envelope $e) => $filter === \get_class($e->getMessage()));
}

$filter = self::normalizeFilter($filter);

foreach ($this->envelopes as $envelope) {
if ($filter($envelope)) {
return new TestEnvelope($envelope);
}
}

throw new \RuntimeException('No envelopes found.');
}

/**
* The messages extracted from envelopes.
*
Expand All @@ -79,13 +105,49 @@ public function messages(?string $class = null): array
return \array_values(\array_filter($messages, static fn(object $message) => $class === \get_class($message)));
}

/**
* @return TestEnvelope[]
*/
public function all(): array
{
return \iterator_to_array($this);
}

public function getIterator(): \Iterator
{
return new \ArrayIterator($this->envelopes);
foreach ($this->envelopes as $envelope) {
yield new TestEnvelope($envelope);
}
}

public function count(): int
{
return \count($this->envelopes);
}

private static function normalizeFilter(callable $filter): callable
{
$function = new \ReflectionFunction(\Closure::fromCallable($filter));

if (!$parameter = $function->getParameters()[0] ?? null) {
return $filter;
}

if (!$type = $parameter->getType()) {
return $filter;
}

if (!$type instanceof \ReflectionNamedType || $type->isBuiltin() || Envelope::class === $type->getName()) {
return $filter;
}

// user used message class name as type-hint
return function(Envelope $envelope) use ($filter, $type) {
if ($type->getName() !== \get_class($envelope->getMessage())) {
return false;
}

return $filter($envelope->getMessage());
};
}
}
40 changes: 40 additions & 0 deletions src/TestEnvelope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Zenstruck\Messenger\Test;

use PHPUnit\Framework\Assert as PHPUnit;
use Symfony\Component\Messenger\Envelope;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @mixin Envelope
*/
final class TestEnvelope
{
private Envelope $envelope;

public function __construct(Envelope $envelope)
{
$this->envelope = $envelope;
}

public function __call($name, $arguments)
{
return $this->envelope->{$name}(...$arguments);
}

public function assertHasStamp(string $class): self
{
PHPUnit::assertNotEmpty($this->envelope->all($class));

return $this;
}

public function assertNotHasStamp(string $class): self
{
PHPUnit::assertEmpty($this->envelope->all($class));

return $this;
}
}
54 changes: 52 additions & 2 deletions tests/InteractsWithMessengerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Zenstruck\Messenger\Test\InteractsWithMessenger;
use Zenstruck\Messenger\Test\TestEnvelope;
use Zenstruck\Messenger\Test\Tests\Fixture\Messenger\MessageA;
use Zenstruck\Messenger\Test\Tests\Fixture\Messenger\MessageAHandler;
use Zenstruck\Messenger\Test\Tests\Fixture\Messenger\MessageB;
Expand Down Expand Up @@ -111,6 +113,52 @@ public function disabling_intercept_with_items_on_queue_processes_all(): void
$this->assertCount(1, self::$container->get(MessageBHandler::class)->messages);
}

/**
* @test
*/
public function can_access_envelope_collection_items_via_first(): void
{
self::bootKernel();

self::$container->get(MessageBusInterface::class)->dispatch($m1 = new MessageA());
self::$container->get(MessageBusInterface::class)->dispatch($m2 = new MessageB());
self::$container->get(MessageBusInterface::class)->dispatch($m3 = new MessageA(true));

$this->messenger()->queue()->assertCount(3);

$this->assertSame($m1, $this->messenger()->queue()->first()->getMessage());
$this->assertSame($m2, $this->messenger()->queue()->first(MessageB::class)->getMessage());
$this->assertSame($m3, $this->messenger()->queue()->first(fn(Envelope $e) => $e->getMessage()->fail)->getMessage());
$this->assertSame($m3, $this->messenger()->queue()->first(fn($e) => $e->getMessage()->fail)->getMessage());
$this->assertSame($m3, $this->messenger()->queue()->first(fn(MessageA $m) => $m->fail)->getMessage());
}

/**
* @test
*/
public function envelope_collection_first_throws_exception_if_no_match(): void
{
self::bootKernel();

$this->expectException(\RuntimeException::class);

$this->messenger()->queue()->first();
}

/**
* @test
*/
public function can_make_stamp_assertions_on_test_envelope(): void
{
self::bootKernel();

self::$container->get(MessageBusInterface::class)->dispatch(new MessageA(), [new DelayStamp(1000)]);
self::$container->get(MessageBusInterface::class)->dispatch(new MessageB());

$this->messenger()->queue()->first()->assertHasStamp(DelayStamp::class);
$this->messenger()->queue()->first(MessageB::class)->assertNotHasStamp(DelayStamp::class);
}

/**
* @test
*/
Expand Down Expand Up @@ -250,17 +298,19 @@ public function can_access_message_objects_on_queue(): void
/**
* @test
*/
public function can_access_envelopes_on_queue(): void
public function can_access_envelopes_on_envelope_collection(): void
{
self::bootKernel();

self::$container->get(MessageBusInterface::class)->dispatch($m1 = new MessageA());
self::$container->get(MessageBusInterface::class)->dispatch($m2 = new MessageB());
self::$container->get(MessageBusInterface::class)->dispatch($m3 = new MessageA());

$messages = \array_map(fn(Envelope $envelope) => $envelope->getMessage(), \iterator_to_array($this->messenger()->queue()));
$messages = \array_map(fn(TestEnvelope $envelope) => $envelope->getMessage(), $this->messenger()->queue()->all());
$messagesFromIterator = \array_map(fn(TestEnvelope $envelope) => $envelope->getMessage(), \iterator_to_array($this->messenger()->queue()));

$this->assertSame([$m1, $m2, $m3], $messages);
$this->assertSame([$m1, $m2, $m3], $messagesFromIterator);
}

/**
Expand Down