diff --git a/CHANGELOG.md b/CHANGELOG.md index 277b7ab..1a1ebc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 2.6.2 - 2016-01-12 + +### Added + +- [#19](https://github.com/zendframework/zend-eventmanager/pull/19) adds a new + trait, `Zend\EventManager\Test\EventListenerIntrospectionTrait`, intended for + composition in unit tests. It provides a number of methods that can be used + to retrieve listeners with or without associated priority, and the assertion + `assertListenerAtPriority(callable $listener, $priority, $event, EventManager $events, $message = '')`, + which can be used for testing that a listener was registered at the specified + priority with the specified event. + + The features in this patch are intended to facilitate testing against both + version 2 and version 3 of zend-eventmanager, as it provides a consistent API + for retrieving lists of events and listeners between the two versions. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + ## 2.6.0 - 2015-09-29 ### Added diff --git a/src/Test/EventListenerIntrospectionTrait.php b/src/Test/EventListenerIntrospectionTrait.php new file mode 100644 index 0000000..f1d7d04 --- /dev/null +++ b/src/Test/EventListenerIntrospectionTrait.php @@ -0,0 +1,136 @@ +getEvents(); + } + + /** + * Retrieve an interable list of listeners for an event. + * + * Given an event and an event manager, returns an iterator with the + * listeners for that event, in priority order. + * + * If $withPriority is true, the key values will be the priority at which + * the given listener is attached. + * + * Do not pass $withPriority if you want to cast the iterator to an array, + * as many listeners will likely have the same priority, and thus casting + * will collapse to the last added. + * + * @param string $event + * @param EventManager $events + * @param bool $withPriority + * @return \Traversable + */ + private function getListenersForEvent($event, EventManager $events, $withPriority = false) + { + $listeners = $events->getListeners($event); + return $this->traverseListeners($listeners, $withPriority); + } + + /** + * Assert that a given listener exists at the specified priority. + * + * @param callable $expectedListener + * @param int $expectedPriority + * @param string $event + * @param EventManager $events + * @param string $message Failure message to use, if any. + */ + private function assertListenerAtPriority( + callable $expectedListener, + $expectedPriority, + $event, + EventManager $events, + $message = '' + ) { + $message = $message ?: sprintf( + 'Listener not found for event "%s" and priority %d', + $event, + $expectedPriority + ); + $listeners = $this->getListenersForEvent($event, $events, true); + $found = false; + foreach ($listeners as $priority => $listener) { + if ($listener === $expectedListener + && $priority === $expectedPriority + ) { + $found = true; + break; + } + } + Assert::assertTrue($found, $message); + } + + /** + * Returns an indexed array of listeners for an event. + * + * Returns an indexed array of listeners for an event, in priority order. + * Priority values will not be included; use this only for testing if + * specific listeners are present, or for a count of listeners. + * + * @param string $event + * @param EventManager $events + * @return callable[] + */ + private function getArrayOfListenersForEvent($event, EventManager $events) + { + return iterator_to_array($this->getListenersForEvent($event, $events)); + } + + /** + * Generator for traversing listeners in priority order. + * + * @param PriorityQueue $listeners + * @param bool $withPriority When true, yields priority as key. + */ + public function traverseListeners(PriorityQueue $queue, $withPriority = false) + { + foreach ($queue as $handler) { + $listener = $handler->getCallback(); + if ($withPriority) { + $priority = (int) $handler->getMetadatum('priority'); + yield $priority => $listener; + } else { + yield $listener; + } + } + } +} diff --git a/test/Test/EventListenerIntrospectionTraitTest.php b/test/Test/EventListenerIntrospectionTraitTest.php new file mode 100644 index 0000000..ec27fa2 --- /dev/null +++ b/test/Test/EventListenerIntrospectionTraitTest.php @@ -0,0 +1,199 @@ +events = new EventManager(); + } + + public function testGetEventsFromEventManagerReturnsEventList() + { + // @codingStandardsIgnoreStart + $this->events->attach('foo', function ($e) {}); + $this->events->attach('bar', function ($e) {}); + $this->events->attach('baz', function ($e) {}); + // @codingStandardsIgnoreEnd + + $this->assertEquals(['foo', 'bar', 'baz'], $this->getEventsFromEventManager($this->events)); + } + + public function testGetListenersForEventReturnsIteratorOfListenersForEventInPriorityOrder() + { + // @codingStandardsIgnoreStart + $callback1 = function ($e) {}; + $callback2 = function ($e) {}; + $callback3 = function ($e) {}; + $callback4 = function ($e) {}; + $callback5 = function ($e) {}; + // @codingStandardsIgnoreEnd + + $this->events->attach('foo', $callback5, 1); + $this->events->attach('foo', $callback1, 2); + $this->events->attach('foo', $callback4, 3); + $this->events->attach('foo', $callback3, 4); + $this->events->attach('foo', $callback2, 5); + + $listeners = $this->getListenersForEvent('foo', $this->events); + $this->assertInstanceOf(Traversable::class, $listeners); + $listeners = iterator_to_array($listeners); + + $this->assertEquals([ + $callback5, + $callback1, + $callback4, + $callback3, + $callback2, + ], $listeners); + } + + public function testGetListenersForEventReturnsIteratorOfListenersInAttachmentOrderWhenSamePriority() + { + // @codingStandardsIgnoreStart + $callback1 = function ($e) {}; + $callback2 = function ($e) {}; + $callback3 = function ($e) {}; + $callback4 = function ($e) {}; + $callback5 = function ($e) {}; + // @codingStandardsIgnoreEnd + + $this->events->attach('foo', $callback5); + $this->events->attach('foo', $callback1); + $this->events->attach('foo', $callback4); + $this->events->attach('foo', $callback3); + $this->events->attach('foo', $callback2); + + $listeners = $this->getListenersForEvent('foo', $this->events); + $this->assertInstanceOf(Traversable::class, $listeners); + $listeners = iterator_to_array($listeners); + + $this->assertEquals([ + $callback5, + $callback1, + $callback4, + $callback3, + $callback2, + ], $listeners); + } + + public function testGetListenersForEventCanReturnPriorityKeysWhenRequested() + { + // @codingStandardsIgnoreStart + $callback1 = function ($e) {}; + $callback2 = function ($e) {}; + $callback3 = function ($e) {}; + $callback4 = function ($e) {}; + $callback5 = function ($e) {}; + // @codingStandardsIgnoreEnd + + $this->events->attach('foo', $callback5, 1); + $this->events->attach('foo', $callback1, 2); + $this->events->attach('foo', $callback4, 3); + $this->events->attach('foo', $callback3, 4); + $this->events->attach('foo', $callback2, 5); + + $listeners = $this->getListenersForEvent('foo', $this->events, true); + $this->assertInstanceOf(Traversable::class, $listeners); + $listeners = iterator_to_array($listeners); + + $this->assertEquals([ + 1 => $callback5, + 2 => $callback1, + 3 => $callback4, + 4 => $callback3, + 5 => $callback2, + ], $listeners); + } + + public function testGetArrayOfListenersForEventReturnsArrayOfListenersInPriorityOrder() + { + // @codingStandardsIgnoreStart + $callback1 = function ($e) {}; + $callback2 = function ($e) {}; + $callback3 = function ($e) {}; + $callback4 = function ($e) {}; + $callback5 = function ($e) {}; + // @codingStandardsIgnoreEnd + + $this->events->attach('foo', $callback5, 1); + $this->events->attach('foo', $callback1, 1); + $this->events->attach('foo', $callback4, 3); + $this->events->attach('foo', $callback3, 2); + $this->events->attach('foo', $callback2, 2); + + $listeners = $this->getArrayOfListenersForEvent('foo', $this->events); + $this->assertInternalType('array', $listeners); + + $this->assertEquals([ + $callback5, + $callback1, + $callback3, + $callback2, + $callback4, + ], $listeners); + } + + public function testAssertListenerAtPriorityPassesWhenListenerIsFound() + { + // @codingStandardsIgnoreStart + $callback = function ($e) {}; + // @codingStandardsIgnoreEnd + + $this->events->attach('foo', $callback, 7); + + $this->assertListenerAtPriority($callback, 7, 'foo', $this->events); + } + + public function testAssertListenerAtPriorityFailsWhenListenerIsNotFound() + { + // @codingStandardsIgnoreStart + $event = 'foo'; + $listener = function ($e) {}; + $priority = 7; + $this->events->attach($event, $listener, $priority); + + $alternate = function ($e) {}; + + $permutations = [ + 'different-listener' => ['listener' => $alternate, 'priority' => $priority, 'event' => $event], + 'different-priority' => ['listener' => $listener, 'priority' => $priority + 1, 'event' => $event], + 'different-event' => ['listener' => $listener, 'priority' => $priority, 'event' => $event . '-FOO'], + ]; + // @codingStandardsIgnoreEnd + + foreach ($permutations as $case => $arguments) { + try { + $this->assertListenerAtPriority( + $arguments['listener'], + $arguments['priority'], + $arguments['event'], + $this->events + ); + $this->fail('assertListenerAtPriority assertion had a false positive for case ' . $case); + } catch (ExpectationFailedException $e) { + $this->assertContains(sprintf( + 'Listener not found for event "%s" and priority %d', + $arguments['event'], + $arguments['priority'] + ), $e->getMessage(), sprintf('Assertion failure message was unexpected: %s', $e->getMessage())); + } + } + } +}