Actions

Unit Testing -- Data Driven Example

From Joomla! Documentation

Revision as of 15:18, 12 May 2008 by Instance (Talk | contribs)

Contents

Anatomy of a Data-Driven Test

This expands on the basics given in (add link). In this example, we're interested in running a number of tests on the JRequest::getVar() method, each with different parameters. While it's possible to just write one test and then copy it once for each test, this would create a lot of duplicate code, add a lot of needless lines to the test file, and make it harder to organize test cases.

We also need to introduce a mock class with this example. Let's dig into the test to see why.

Defining the Test Criteria for JRequest::getVar()

We'll start by getting an understanding of what actually needs to be tested when we say "test getVar". Looking at the code, we can learn a few things:

  • The getVar method uses the $hash parameter to determine the source of the variable. This means we should test all expected values for $hash and a few invalid ones as well.
  • The class maintains a cache in $GLOBALS['_JREQUEST']. This means we should check to ensure that the hash works as expected, and that we need to pay some attention to ensuring that the cache is in a known state when we run a test.
  • getVar() calls the protected method _cleanVar() to do the real work. Depending on the value of the $mask parameter, this might result in a call to JFilterInput::clean(). We learn two things. First, we need to test various values for $mask (now in combination with $hash). Second, since testing JFilterInput is the responsibility of a unit test for that class, we will need to create a mock JFilterInput class, so we can isolate JRequest.

Building a Mock Class

Defining a Mock Class for JFilterInput

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.

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: 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 (this will 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. For each test, we define 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(). The below is the complete code for our mock, which can be found in /unittest/tests/libraries/joomla/environment/JFilterInput-mock-general.php


/**
 * 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;
        }
 
}

The mockSetup() method defines the expectations for the calls to clean() during a test. This is simple. 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).

The mockTearDown() method is called at the end of the test. If an expectation has been met, 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.

Of the two methods we need to mock, getInstance() simply returns a new 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. Let's put it to use.

(saving this, continuing work)

Sample general-purpose mock: /unittest/tests/libraries/joomla/plugin/JPluginHelper-mock-general.php