Runkit + PHPUnit = 100% test coverage
Dear colleagues.
One of the indirect indicators of code quality is considered code coverage - the degree to which it is covered by tests (as a rule, we mean unit tests). In most cases, coverage is the ratio of the number of lines of code in which control falls during the test run to the total number of significant (not a comment, empty line, or, for example, one curly brace that indicates the beginning or end of a block) lines of the module code.
Another condition for good tests is the absence of side effects, such as creating / deleting files, establishing network connections, writing to ports, etc.
However, when it comes to a module that interacts with the outside world, these two requirements conflict. And okay, when it comes to file operations, when vfsStream comes to the rescue . But what to do when you need to test, say, direct work with sockets or code that uses curl_ * functions?
Under the cut you will find my solution and, as a bonus, another OPP wrapper for the curl, fully covered with tests.
You can use Runkit to write unit tests for such code .. I wrote an extension of PHPUnit's TestCase, which, with the help of the rankit, allows you to replace built-in functions using the full power of asserts and mocks from Sebastian Bergman. The heir is so simple that I allow myself to bring his code here in its entirety.
Attentive readers have already noticed the simplest example of use in the docking block of the class, I will gladly give explanations, if they are needed, in the comments to the topic. A slightly more complicated use case, testing the transfer of parameters by reference, you will find in the tests of the OOP wrapper to the Kurl that I promised .
Thank you, I will be grateful for the criticism.
One of the indirect indicators of code quality is considered code coverage - the degree to which it is covered by tests (as a rule, we mean unit tests). In most cases, coverage is the ratio of the number of lines of code in which control falls during the test run to the total number of significant (not a comment, empty line, or, for example, one curly brace that indicates the beginning or end of a block) lines of the module code.
Another condition for good tests is the absence of side effects, such as creating / deleting files, establishing network connections, writing to ports, etc.
However, when it comes to a module that interacts with the outside world, these two requirements conflict. And okay, when it comes to file operations, when vfsStream comes to the rescue . But what to do when you need to test, say, direct work with sockets or code that uses curl_ * functions?
Under the cut you will find my solution and, as a bonus, another OPP wrapper for the curl, fully covered with tests.
You can use Runkit to write unit tests for such code .. I wrote an extension of PHPUnit's TestCase, which, with the help of the rankit, allows you to replace built-in functions using the full power of asserts and mocks from Sebastian Bergman. The heir is so simple that I allow myself to bring his code here in its entirety.
- namespace Base\UnitTest;
-
- /**
- * Test case which enables overriding of functions with runkit.
- *
- * 1. To override a number of system function do
- *
- * $mock = $this->runkitMockFunctions(array(...));
- *
- * 2. To define expected behaivour use $mock as an ordinary phpunit mock object.
- *
- * 3. To revert overridden functions back call $this->runkitRevertAll();
- *
- * Example:
- *
- * class MyCurlTest
- * extends \Base\UnitTest\RunkitTestCase
- * {
- * protected $mock;
- *
- * protected function setUp()
- * {
- * $this->mock = $this->runkitMockFunctions(array(
- * 'curl_init',
- * 'curl_close',
- * ));
- * }
- *
- * protected function tearDown()
- * {
- * $this->runkitRevertAll();
- * }
- *
- * public function testInitClose()
- * {
- * $this->mock
- * ->expects($this->at(0))
- * ->method('curl_init')
- * ->with()
- * ->will($this->returnValue('my_handle'));
- *
- * $this->mock
- * ->expects($this->at(1))
- * ->method('curl_close')
- * ->with('my_handle');
- *
- * $handle = curl_init();
- * $this->assertEquals('my_handle', $handle);
- * curl_close($handle);
- * }
- * }
- *
- * @package Base\UnitTest
- * @version $id$
- * @author Alexey Karapetov
- */
- abstract class RunkitTestCase
- extends \PHPUnit_Framework_TestCase
- {
- private static $mockedFunctions = array();
-
- const BACKUP_SUFFIX = '_runkit_mocker_backup';
-
- /**
- * Method to call from overridden functions.
- * Calls given mock's method with given arguments.
- *
- * @param string $method Mock's method to call
- * @param array $args Arguments to pass to @link $method
- * @return void
- */
- public static function call($func, array $args)
- {
- return call_user_func_array(array(self::$mockedFunctions[$func], $func), $args);
- }
-
- /**
- * Mark test skipped if runkit is not enabled
- *
- * @return void
- */
- protected function skipTestIfNoRunkit()
- {
- if (!extension_loaded('runkit'))
- {
- $this->markTestSkipped('Runkit extension is not loaded');
- }
- }
-
- /**
- * Override given functions with mock
- *
- * @param array $funcList Functions to override
- * @return stdClass Mock object
- */
- protected function runkitMockFunctions(array $funcList)
- {
- $this->skipTestIfNoRunkit();
-
- $mock = $this->getMock('stdClass', $funcList);
-
- foreach ($funcList as $func)
- {
- $this->runkitOverride($func, '', 'return ' . __CLASS__ . "::call('{$func}', func_get_args());", $mock);
- }
-
- return $mock;
- }
-
- /**
- * Override function
- *
- * @param string $func
- * @param string $args
- * @param string $body
- * @param mixed $mock Mock object for the function
- * @return void
- */
- protected function runkitOverride($func, $args, $body, $mock = null)
- {
- $this->skipTestIfNoRunkit();
-
- if (array_key_exists($func, self::$mockedFunctions))
- {
- throw new \RuntimeException("Function '{$func}' is marked as mocked already");
- }
- self::$mockedFunctions[$func] = $mock;
- \runkit_function_copy($func, $func . self::BACKUP_SUFFIX);
- \runkit_function_redefine($func, $args, $body);
- }
-
-
- /**
- * Revert previously overridden function
- *
- * @param string $func
- * @return void
- */
- protected function runkitRevert($func)
- {
- $this->skipTestIfNoRunkit();
-
- if (!array_key_exists($func, self::$mockedFunctions))
- {
- throw new \RuntimeException("Function '{$func}' is not marked as mocked");
- }
- unset(self::$mockedFunctions[$func]);
-
- \runkit_function_remove($func);
- \runkit_function_copy($func . self::BACKUP_SUFFIX, $func);
- \runkit_function_remove($func . self::BACKUP_SUFFIX);
- }
-
- /**
- * Revert all previously overridden functions
- *
- * @return void
- */
- protected function runkitRevertAll()
- {
- foreach (array_keys(self::$mockedFunctions) as $func)
- {
- $this->runkitRevert($func);
- }
- }
- }
* This source code was highlighted with Source Code Highlighter.
Attentive readers have already noticed the simplest example of use in the docking block of the class, I will gladly give explanations, if they are needed, in the comments to the topic. A slightly more complicated use case, testing the transfer of parameters by reference, you will find in the tests of the OOP wrapper to the Kurl that I promised .
Thank you, I will be grateful for the criticism.