diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f7487db..0ae6870e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ * Destructors (`__destruct`) are stubbed out where it makes sense * Allow passing a closure argument to `withArgs()` to validate multiple arguments at once. -* `Mockery\Adapter\Phpunit\TestListener` has been removed because it - incorrectly marks some tests as risky. +* `Mockery\Adapter\Phpunit\TestListener` has been rewritten because it + incorrectly marked some tests as risky. It will no longer verify mock + expectations but instead check that tests do that themselves. ## 0.9.4 (XXXX-XX-XX) diff --git a/docs/reference/phpunit_integration.rst b/docs/reference/phpunit_integration.rst index b4edd9691..dc1b8eb5f 100644 --- a/docs/reference/phpunit_integration.rst +++ b/docs/reference/phpunit_integration.rst @@ -67,7 +67,7 @@ Composer generated autoloader file: the file name is updated for all your projects.) To integrate Mockery into PHPUnit and avoid having to call the close method -and have Mockery remove itself from code coverage reports, use this in you +and have Mockery remove itself from code coverage reports, use this in your suite: .. code-block:: php @@ -87,6 +87,38 @@ An alternative is to use the supplied trait: } +Mockery provides a PHPUnit listener that makes tests fail if +``Mockery::close()`` has not been called. It can help identify tests where +you've forgotten to include the trait or extend the ``MockeryTestCase``. + +If you are using PHPUnit's XML configuration approach, you can include the +following to load the ``TestListener``: + +.. code-block:: xml + + + + + +Make sure Composer's or Mockery's autoloader is present in the bootstrap file +or you will need to also define a "file" attribute pointing to the file of the +``TestListener`` class. + +If you are creating the test suite programmatically you may add the listener +like this: + +.. code-block:: php + + // Create the suite. + $suite = new PHPUnit_Framework_TestSuite(); + + // Create the listener and add it to the suite. + $result = new PHPUnit_Framework_TestResult(); + $result->addListener(new \Mockery\Adapter\Phpunit\TestListener()); + + // Run the tests. + $suite->run($result); + .. caution:: PHPUnit provides a functionality that allows diff --git a/library/Mockery/Adapter/Phpunit/TestListener.php b/library/Mockery/Adapter/Phpunit/TestListener.php new file mode 100644 index 000000000..ec9b88a73 --- /dev/null +++ b/library/Mockery/Adapter/Phpunit/TestListener.php @@ -0,0 +1,64 @@ +getStatus() !== \PHPUnit_Runner_BaseTestRunner::STATUS_PASSED) { + // If the test didn't pass there is no guarantee that + // verifyMockObjects and assertPostConditions have been called. + // And even if it did, the point here is to prevent false + // negatives, not to make failing tests fail for more reasons. + return; + } + + try { + // The self() call is used as a sentinel. Anything that throws if + // the container is closed already will do. + \Mockery::self(); + } catch (\LogicException $_) { + return; + } + + $e = new \PHPUnit_Framework_ExpectationFailedException(sprintf( + "Mockery's expectations have not been verified. Make sure that \Mockery::close() is called at the end of the test. Consider using %s\MockeryPHPUnitIntegration or extending %s\MockeryTestCase.", + __NAMESPACE__, + __NAMESPACE__ + )); + $result = $test->getTestResultObject(); + $result->addFailure($test, $e, $time); + } +} diff --git a/tests/Mockery/Adapter/Phpunit/TestListenerTest.php b/tests/Mockery/Adapter/Phpunit/TestListenerTest.php new file mode 100644 index 000000000..83180f48b --- /dev/null +++ b/tests/Mockery/Adapter/Phpunit/TestListenerTest.php @@ -0,0 +1,75 @@ +container = \Mockery::getContainer(); + $this->listener = new \Mockery\Adapter\Phpunit\TestListener(); + $this->testResult = new \PHPUnit_Framework_TestResult(); + $this->test = new \Mockery_Adapter_Phpunit_EmptyTestCase(); + + $this->test->setTestResultObject($this->testResult); + $this->testResult->addListener($this->listener); + + $this->assertTrue($this->testResult->wasSuccessful(), 'sanity check: empty test results should be considered successful'); + } + + public function testSuccessOnClose() + { + $mock = $this->container->mock(); + $mock->shouldReceive('bar')->once(); + $mock->bar(); + + // This is what MockeryPHPUnitIntegration and MockeryTestCase trait + // will do. We intentionally call the static close method. + $this->test->addToAssertionCount($this->container->mockery_getExpectationCount()); + \Mockery::close(); + + $this->listener->endTest($this->test, 0); + $this->assertTrue($this->testResult->wasSuccessful(), 'expected test result to indicate success'); + } + + public function testFailureOnMissingClose() + { + $mock = $this->container->mock(); + $mock->shouldReceive('bar')->once(); + + $this->listener->endTest($this->test, 0); + $this->assertFalse($this->testResult->wasSuccessful(), 'expected test result to indicate failure'); + + // Satisfy the expectation and close the global container now so we + // don't taint the environment. + $mock->bar(); + \Mockery::close(); + } +} + +class Mockery_Adapter_Phpunit_EmptyTestCase extends PHPUnit_Framework_TestCase +{ + public function getStatus() + { + return \PHPUnit_Runner_BaseTestRunner::STATUS_PASSED; + } +}