Welcome Guest, Not a member yet? Register   Sign In
Unit testing, code coverage, and undefined functions
#1

I'm not much of a unit tester, but see the value, and feel the need to be proficient at it.

So, because of another thread here on the forum, I thought I'd try to make a little REST class, and unit test it with phpunit.

Code coverage is pretty good, but there are some things that I'm not sure about, and one is phpunit's inability to use the PHP function getallheaders. As a work-around, I just check if the function exists before trying to call it, and during the tests I set the headers manually. I found some explanations online as to why getallheaders isn't available, but assuming it would be under normal circumstances.

I know it's not required to have 100% code coverage, and wondering if this is an instance where it would be appropriate to let it be less than 100%.
Reply
#2

It really depends on what you're testing. I've heard 80% is the magic number from several different sources over the years. That is enough to feel pretty safe. And after 80% there seems to be diminishing returns.

It's more important, I think, to try to cover as many edge cases and "bad things" that can happen, instead of focusing on total coverage number. But the 80% seems fairly accurate in my usage so far.
Reply
#3

(07-28-2017, 12:42 PM)kilishan Wrote: It really depends on what you're testing. I've heard 80% is the magic number from several different sources over the years. That is enough to feel pretty safe. And after 80% there seems to be diminishing returns.

It's more important, I think, to try to cover as many edge cases and "bad things" that can happen, instead of focusing on total coverage number. But the 80% seems fairly accurate in my usage so far.

The report says that coverage of functions and methods is at 62.5%. Coverage of lines is 95%.

This is what I'm playing around with:

Library

PHP Code:
<?php 
/**
 * The goal of this simple class is to make available the
 * necessary information and data related to REST APIs.
 
 # -------------------------
 # Basic example usage 
    $REST = new REST;
    if( $REST->method == 'put' )
        $data = $REST->input_data;
 # -------------------------

 # -------------------------
 # Example allowing POST as PATCH 
 # overridden by $_POST['_method'] = 'PATCH'
    $REST = new REST([
       '_override_by_post_var' => TRUE
    ]);
    if( $REST->method == 'patch' )
        $data = $REST->input_data;
 # -------------------------

 # -------------------------
 # Example allowing POST as PUT 
 # overridden by X-HTTP-Method-Override header = 'PUT'
    $REST = new REST([
       '_override_by_header' => TRUE
    ]);
    if( $REST->method == 'put' )
        $data = $REST->input_data;
 # -------------------------

 * Note: no attempt has been made to validate or sanitize 
 *       the data available in $REST->input_data.
 */

class REST {

    
/**
     * The detected HTTP request method
     */
    
public $method '';

    
/**
     * The detected content type
     */
    
public $content_type '';

    
/**
     * Input data from PUT and PATCH requests
     */
    
public $input_data = [];

    
/**
     * Allowed HTTP methods
     */
    
protected $_allowed_methods = ['get','delete','post','put','options','patch','head'];

    
/**
     * Overridable HTTP methods
     */
    
protected $_overridable_methods = ['post'];

    
/**
     * Replacement HTTP methods
     */
    
protected $_replacement_methods = ['put','patch'];

    
/**
     * Allow method overrides by post variable
     */
    
protected $_override_by_post_var FALSE;

    
/**
     * Allow method overrides by header
     */
    
protected $_override_by_header FALSE;

    
/**
     * The request headers
     */
    
protected $_headers = [];

    
/**
     * php://input
     */
    
protected $_php_input NULL;

    
// -----------------------------------------------------------------------

    /**
     * Class constructor
     * -----------------
     *
     * @param  array  class configuration
     */
    
public function __construct$params = [] )
    {
        
// Any key that is a property can be configured.
        
foreach( $params as $key => $value )
        {
            if( 
property_exists$this$key ) )
                
$this->$key $value;
        }

        
/**
         * CLI indicates that we are doing tests through phpunit.
         * Run automatically if we are not doing tests.
         */
        
if( ! ( PHP_SAPI === 'cli' OR defined('STDIN') ) )
            
$this->run();
    }

    
// -----------------------------------------------------------------------

    /**
     * Get all headers, detect method, detect content type, and get input data.
     */
    
public function run()
    {
        
$this->_headers     $this->_get_request_headers();
        
$this->method       $this->_detect_method();
        
$this->content_type $this->_detect_content_type();
        
$this->input_data   $this->_fetch_input_data();
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Get the request headers
     */
    
protected function _get_request_headers()
    {
        
// For whatever reason, getallheaders wasn't available when testing
        
if( function_exists('getallheaders') )
            return 
getallheaders();

        return 
NULL;
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Detect the request method
     * -------------------------
     * A put or patch request could come from an HTML form, 
     * but it would need to use POST as the request method, 
     * so we may allow the post var "_method" to indicate that
     * we're doing a post that should be seen as a put or patch.
     *
     * We may also allow the X-HTTP-Method-Override header to 
     * change the request method to put or patch.
     */
    
protected function _detect_method()
    {
        
$method = isset( $_SERVER['REQUEST_METHOD'] )
            ? 
strtolower$_SERVER['REQUEST_METHOD'] )
            : 
NULL;

        
// Method override ?
        
if( ! is_null$method ) && in_array$method$this->_overridable_methods ) )
        {
            
// Method override by header
            
if( 
                isset( 
$this->_headers['X-HTTP-Method-Override'] ) && 
                
in_arraystrtolower$this->_headers['X-HTTP-Method-Override'] ), $this->_replacement_methods )
            ){
                
$method $this->_override_by_header
                    
strtolower$this->_headers['X-HTTP-Method-Override'] )
                    : 
NULL;
            }

            
// Method override by POST var
            
else if( 
                isset( 
$_POST['_method'] ) && 
                
in_arraystrtolower$_POST['_method'] ), $this->_replacement_methods )
            ){
                
$method $this->_override_by_post_var
                    
strtolower$_POST['_method'] )
                    : 
NULL;
            }
        }

        return 
in_array$method$this->_allowed_methods )
            ? 
$method
            
NULL;
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Try to find the declared mime type for the request
     */
    
protected function _detect_content_type()
    {
        if( isset( 
$_SERVER['CONTENT_TYPE'] ) )
        {
            
$type $_SERVER['CONTENT_TYPE'];

            if( ! empty( 
$type ) )
            {
                
// Only the first part of the header is the mime type
                
return strpos$type';' ) !== FALSE 
                    
currentexplode';'$type ) ) 
                    : 
$type;
            }
        }

        return 
NULL;
    }
    
    
// -----------------------------------------------------------------------

    /**
     * For put or patch requests, the input data will be in php://input,
     * unless this is a POST disguised as a PUT or PATCH.
     */
    
protected function _fetch_input_data()
    {
        
// If there's a post array, just return it
        
if( in_array$this->method, ['put','patch'] ) && ! empty( $_POST ) )
            return 
$_POST;

        
// Otherwise, check PHP's input stream
        
if( in_array$this->method, ['put','patch'] ) )
        {
            
$input $this->_cache_php_input();

            if( ! empty( 
$input ) )
                return 
$this->_process_php_input();
        }

        return 
NULL;
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Cache contents of php://input
     */
    
protected function _cache_php_input()
    {
        ! 
is_null$this->_php_input ) OR 
            
$this->_php_input file_get_contents('php://input');

        return 
$this->_php_input;
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Parse, decode, unserialize, or otherwise process the php input.
     */
    
protected function _process_php_input()
    {
        switch( 
$this->content_type )
        {
            case 
'application/x-www-form-urlencoded':
                
parse_str$this->_php_input $vars );
                break;
            case 
'application/json':
                
$vars json_decode$this->_php_inputTRUE);
                break;
            case 
'application/vnd.php.serialized':
                
$vars unserialize$this->_php_input );
                break;
            default:
                
$vars $this->_php_input;
        }
        
        return 
$vars;
    }
    
    
// -----------------------------------------------------------------------

}

/* End of file REST.php */ 


The test:

PHP Code:
<?php
// Install: 
//         wget https://phar.phpunit.de/phpunit.phar
//         chmod +x phpunit.phar
//         sudo mv phpunit.phar /usr/bin/phpunit
//         phpunit --version
// Run all tests in /test/: 
//         phpunit

use PHPUnit\Framework\TestCase;

class 
RESTTest extends TestCase {

    
/**
     * Setup for tests
     */
    
public function setup()
    {
        require_once( 
__DIR__ '/../application/libraries/REST.php' );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Check that the request method is detected correctly 
     * from $_SERVER['REQUEST_METHOD'].
     */
    
public function testRequestMethodDetectionFromHeader()
    {
        
$REST = new REST;
        
        
$_SERVER['REQUEST_METHOD'] = 'PATCH';

        
$this->assertEquals(
            
'patch',
            
$this->invokeMethod$REST'_detect_method' )
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Check that the request method detection returns NULL
     * if not in the set of allowed methods.
     */
    
public function testRequestMethodDetectionFailsWhenMethodNotAllowed()
    {
        
$REST = new REST;
        
        
$_SERVER['REQUEST_METHOD'] = 'XXX';

        
$this->assertEquals(
            
NULL,
            
$this->invokeMethod$REST'_detect_method' )
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Check that the request method is detected correctly 
     * when overridden by post var, $_POST['_method'].
     * This requires that the config for post variable method
     * override set to TRUE when creating the REST object.
     */
    
public function testRequestMethodDetectionWhenOverriddenByPostVar()
    {
        
$REST = new REST([
            
'_override_by_post_var' => TRUE
        
]);
        
        
$_SERVER['REQUEST_METHOD'] = 'POST';
        
$_POST['_method'         'PUT';

        
$this->assertEquals(
            
'put',
            
$this->invokeMethod$REST'_detect_method' )
        );
    }
    
    
// -----------------------------------------------------------------------
    
    /**
     * Check that the request method override by post var fails
     * if post var override is not allowed. (method override not set to TRUE)
     */
    
public function testRequestMethodOverriddenByPostVarFailsWhenNotAllowed()
    {
        
$REST = new REST();
        
        
$_SERVER['REQUEST_METHOD'] = 'POST';
        
$_POST['_method'         'PUT';

        
$this->assertEquals(
            
NULL,
            
$this->invokeMethod$REST'_detect_method' )
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Check that the request method is detected correctly 
     * when overridden by custom header, X-HTTP-Method-Override.
     * This requires that the config for header method
     * override set to TRUE when creating the REST object.
     */
    
public function testRequestMethodDetectionWhenOverriddenByHeader()
    {
        
$REST = new REST([
            
'_override_by_header' => TRUE
        
]);
        
        
$_SERVER['REQUEST_METHOD'] = 'POST';
        
$this->setValue$REST'_headers', ['X-HTTP-Method-Override' => 'PUT'] );

        
$this->assertEquals(
            
'put',
            
$this->invokeMethod$REST'_detect_method' )
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Check that the request method override by header fails
     * if header override is not allowed. (method override not set to TRUE)
     */
    
public function testRequestMethodOverriddenByHeaderFailsWhenNotAllowed()
    {
        
$REST = new REST();
        
        
$_SERVER['REQUEST_METHOD'] = 'POST';
        
$this->setValue$REST'_headers', ['X-HTTP-Method-Override' => 'PUT'] );

        
$this->assertEquals(
            
NULL,
            
$this->invokeMethod$REST'_detect_method' )
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Detect the content type.
     */
    
public function testContentTypeDetections()
    {
        
$REST = new REST;
        
        
$_SERVER['CONTENT_TYPE'] = 'application/json; charset=utf-8';

        
$this->assertEquals(
            
'application/json',
            
$this->invokeMethod$REST'_detect_content_type' )
        );

        
$_SERVER['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';

        
$this->assertEquals(
            
'application/x-www-form-urlencoded',
            
$this->invokeMethod$REST'_detect_content_type' )
        );

        unset( 
$_SERVER['CONTENT_TYPE'] );

        
$this->assertEquals(
            
NULL,
            
$this->invokeMethod$REST'_detect_content_type' )
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * If method override is allowing POST to be PUT or PATCH,
     * input data would be $_POST.
     */
    
public function testInputDataFromPostArray()
    {
        
$REST = new REST([
            
'_override_by_post_var' => TRUE
        
]);

        
$_SERVER['REQUEST_METHOD'] = 'POST';

        
$_POST = [
            
'_method' => 'PUT',
            
'foo'     => 'bar'
        
];

        
$REST->run();

        
$this->assertEquals(
            
'bar',
            
$REST->input_data['foo']
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * If method is not PUT or PATCH then input data should be NULL
     */
    
public function testInputDataReturnsNullWhenWrongMethod()
    {
        
$REST = new REST;

        
$_SERVER['REQUEST_METHOD'] = 'POST';

        
$_POST = [
            
'foo' => 'bar'
        
];

        
$REST->run();

        
$this->assertEquals(
            
NULL,
            
$REST->input_data
        
);
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Make sure input data is set during a PUT request where
     * content is json.
     */
    
public function testInputDataFromJson()
    {
        
$REST = new REST;
        
$this->setValue$REST'_php_input''{"foo":"bar","fiddle":"faddle"}' );
        
        
$_SERVER['REQUEST_METHOD'] = 'PUT';
        
$_SERVER['CONTENT_TYPE'  'application/json';

        
$REST->run();

        
$this->assertEquals(
            
'bar',
            
$REST->input_data['foo']
        );

        
$this->assertEquals(
            
'faddle',
            
$REST->input_data['fiddle']
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Make sure input data is set during a PUT request where
     * content is a url encoded string.
     */
    
public function testInputDataFromUrlEncodedString()
    {
        
$REST = new REST;
        
$this->setValue$REST'_php_input''ding=dong&ching=chong' );
        
        
$_SERVER['REQUEST_METHOD'] = 'PUT';
        
$_SERVER['CONTENT_TYPE'  'application/x-www-form-urlencoded';

        
$REST->run();

        
$this->assertEquals(
            
'dong',
            
$REST->input_data['ding']
        );

        
$this->assertEquals(
            
'chong',
            
$REST->input_data['ching']
        );
    }
    
    
// -----------------------------------------------------------------------
    
    /**
     * Make sure input data is set during a PATCH request where
     * content is a serialized string.
     */
    
public function testInputDataFromSerialization()
    {
        
$REST = new REST;
        
$this->setValue$REST'_php_input'serialize(['foo'=>'bar','baz'=>'qux']) );
        
        
$_SERVER['REQUEST_METHOD'] = 'PATCH';
        
$_SERVER['CONTENT_TYPE'  'application/vnd.php.serialized';

        
$REST->run();

        
$this->assertEquals(
            
'bar',
            
$REST->input_data['foo']
        );

        
$this->assertEquals(
            
'qux',
            
$REST->input_data['baz']
        );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Make sure input data is returned unprocessed
     * if the content type is not one of:
     *     - application/x-www-form-urlencoded
     *     - application/json
     *     - application/vnd.php.serialized
     */
    
public function testInputDataUnprocessedWhenContentTypeNotRecognized()
    {
        
$REST = new REST;
        
$this->setValue$REST'_php_input''ABC123' );
        
        
$_SERVER['REQUEST_METHOD'] = 'PUT';
        
$_SERVER['CONTENT_TYPE'  'application/whatever';

        
$REST->run();

        
$this->assertEquals(
            
'ABC123',
            
$REST->input_data
        
);
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Clean up after each test
     */
    
public function teardown()
    {
        unset( 
$_SERVER['REQUEST_METHOD'] );
        unset( 
$_SERVER['CONTENT_TYPE'] );
        unset( 
$_POST );
    }
    
    
// -----------------------------------------------------------------------

    /**
     * Call protected/private method of a class.
     *
     * @param object &$object    Instantiated object that we will run method on.
     * @param string $methodName Method name to call
     * @param array  $parameters Array of parameters to pass into method.
     *
     * @return mixed Method return.
     */
    
public function invokeMethod(&$object$methodName, array $parameters = array())
    {
        
$reflection = new \ReflectionClass(get_class($object));
        
$method $reflection->getMethod($methodName);
        
$method->setAccessible(true);

        return 
$method->invokeArgs($object$parameters);
    }

    
/**
     * Set values on protected/private properties of a class.
     *
     * @param object &$object    Instantiated object that we will run method on.
     * @param string $property   The property's name
     * @param array  $value      The value to assign to the property.
     *
     * @return void
     */
    
public function setValue(&$object$property$value)
    {
        
$reflection = new \ReflectionClass(get_class($object));
        
$RP $reflection->getProperty($property);
        
$RP->setAccessible(true);
        
$RP->setValue($object$value);
    }
    
    
// -----------------------------------------------------------------------


Reply
#4

> wondering if this is an instance where it would be appropriate to let it be less than 100%.

I think it would be appropriate.
Because we can't test a function which does not exists, and probably you don't have to
test `getallheaders()`.

But if you want to get 100% coverage for the class, you can get it:
1. move `_get_request_headers()` method to another class
2. replace the object which calls `getallheaders()` with an mock object only when you run test.
Reply




Theme © iAndrew 2016 - Forum software by © MyBB