Actions

Difference between revisions of "Unit Testing Mock Objects"

From Joomla! Documentation

m (added Testing and Bug Squad categories)
 
(2 intermediate revisions by 2 users not shown)
Line 115: Line 115:
  
 
That's all we need to support our test for JRequest. The mock is complete.
 
That's all we need to support our test for JRequest. The mock is complete.
 +
 +
=== Using the Mock ===
 +
The last thing to do is to convince the Joomla framework to use our mock class. The unit test platform introduces a custom version of JLoader that simplifies this process. In the mock objects of the prelude to the test class, we add this code:
 +
 +
<source lang="php">
 +
/*
 +
* Mock classes
 +
*/
 +
JLoader::injectMock(
 +
    dirname(__FILE__) . DS . 'JFilterInput-mock-general.php',
 +
    'joomla.filter.filterinput'
 +
);
 +
</source>
 +
 +
This tells our JLoader that any call to jimport('joomla.filter.filterinput') should load the file JFilterInput-mock-general.php from our test directory. Loading this file defines our mock object, and we're ready to go.
 +
 +
[[Category:Automated Testing]][[Category:Testing]][[Category:Bug Squad]]

Latest revision as of 18:29, 14 December 2011

Contents

Unit Testing: Mock Objects

This article discusses the creation of a simple Mock of the JFilterInput class, to meet the requirements of JRequest unit tests. The full code is available at /unittest/tests/libraries/joomla/plugin/JPluginHelper-mock-general.php. The unit tests for JRequest are discussed in the example of a Data-Driven Test.

There are two ways to look at mock classes. If a class is pretty simple, it's possible to write a common class that can be used by a number of unit tests. However if the class is more complex, it's better to write a test-specific mock that only implements the subset of the class that's needed. Mocks are of little use if they become as complex as the object being mocked.

Defining a Mock JFilterInput for testing JRequest

The JFilterInput class falls into the second category: Writing a good general purpose mock for it is too complicated and time consuming, so we will create a subset that only does what we need for our JRequest tests.

JRequest uses two methods in JFilterInput. They are getInstance() and clean().

The getInstance() method is straightforward. It returns an instance a JFilterInput object. Since we aren't interested in the internals of JFilterInput, we have no need to pay attention to the parameters passed to getInstance() in our mock.

For clean(), we're interested in making sure that:

  • JRequest calls clean() with the expected number parameter values, and
  • that clean() is called the appropriate number of times (we use this to verify the cache mechanism).

Our mock also needs to return the clean version of the parameters.

Implementing the JFilterInput Mock Class

This class follows a common pattern for mock objects. Each test defines it's expectations. An expectation is a combination of the expected parameters and the number of calls to a mock method.

This is a simple case because we have only one method to set expectations for: clean(). Below is the complete code for our mock:

/**
 * Mock of JFilterInput for JRequest testing
 */
class JFilterInput
{
        /**
         * Information on the calls expected to the mock object.
         *
         * This array is indexed by a hash of the source and type; each element is
         * an array containing the source, type, expected response and number of
         * expected calls.
         */
        static private $_expectations = array();
 
        /**
         * Returns a reference to a mock input filter singleton
         */
        function & getInstance()
        {
                static $instance;
 
                if (! $instance) {
                        $instance = new JFilterInput();
                }
                return $instance;
        }
 
        /**
         * Stub for the clean method.
         *
         * @access  public
         * @param   mixed   Input string/array-of-string to be 'cleaned'
         * @param   string  Return type for the variable (INT, FLOAT, BOOLEAN, WORD,
         * ALNUM, CMD, BASE64, STRING, ARRAY, PATH, NONE)
         * @return  mixed   Canned response based on table lookup.
         * @static
         */
        function clean($source, $type='string')
        {
                $hash = md5($source . '|' . strtoupper($type));
                if (! isset($this -> _expectations[$hash])) {
                        $this -> _expectations[$hash] = array(
                                'source' => $source,
                                'type' => $type,
                                'result' => null,
                                'count' => 0,
                        );
                }
                --$this -> _expectations[$hash]['count'];
                return $this -> _expectations[$hash]['result'];
        }
 
        function mockReset() {
                $this -> _expectations = array();
        }
 
        function mockSetUp($source, $type, $result, $count = 1) {
                $hash = md5($source . '|' . strtoupper($type));
                $this -> _expectations[$hash] = array(
                        'source' => $source,
                        'type' => $type,
                        'result' => $result,
                        'count' => $count
                );
        }
 
        function mockTearDown() {
                foreach ($this -> _expectations as $hash => $info) {
                        if (! $info['count']) {
                                unset($this -> _expectations[$hash]);
                        }
                }
                if (count($this -> _expectations)) {
                        return $this -> _expectations;
                }
                return true;
        }
 
}

Lets look at the extra methods we added for defining the Mock's expectations.

The mockReset() method simply clears the expectation array.

The mockSetup() method defines the expectations for the calls to clean() at the beginning of the test. This is straightforward. For each combination of clean($source, $type), we specify the what clean should return ($result) and the number of times the method should be called with those parameters ($count). This implementation hashes the parameters to obtain a unique key for the expectation. If the parameters were more complex, we might have to use a different technique such as calling serialize() on object parameters first.

The mockTearDown() method is called at the end of the test. If an expectation has been successful, the value of count will be zero. If our test ran exactly as expected, then all expectations should have a zero count. Expectations with a zero count are removed from the array, and if the resulting array is empty then the test passed. If there are non-zero values then either the method wasn't called as many times as expected (a count greater than zero), or it was called with parameters we didn't anticipate (a negative count). mockTearDown() either returns true on success, or returns a list of mismatched expectations on failure.

Of the two methods we need to mock, getInstance() simply returns a new static instance of our mock. The clean() method matches the input parameters with expectations, either decrementing the count for parameters we expected, or creating a record of parameters we didn't expect.

That's all we need to support our test for JRequest. The mock is complete.

Using the Mock

The last thing to do is to convince the Joomla framework to use our mock class. The unit test platform introduces a custom version of JLoader that simplifies this process. In the mock objects of the prelude to the test class, we add this code:

/*
 * Mock classes
 */
JLoader::injectMock(
    dirname(__FILE__) . DS . 'JFilterInput-mock-general.php',
    'joomla.filter.filterinput'
);

This tells our JLoader that any call to jimport('joomla.filter.filterinput') should load the file JFilterInput-mock-general.php from our test directory. Loading this file defines our mock object, and we're ready to go.