Actions

Difference between revisions of "Unit Testing -- UI Example"

From Joomla! Documentation

(Timeout defense.)
 
m (added Testing and Bug Squad categories)
(2 intermediate revisions by one user not shown)
Line 3: Line 3:
  
 
PHPUnit has an extension to support this, but it's pretty limited. You can extend your test class from PHPUnit_Extensions_OutputTestCase, and then use assertions like $this->expectOutputString('foo') and $this -> expectOutputRegex($regex) to verify that the output contains the string you're looking for. That technique works for simple cases, but if the method you want to test takes complex parameters or generates a lot of code, it's just not enough.
 
PHPUnit has an extension to support this, but it's pretty limited. You can extend your test class from PHPUnit_Extensions_OutputTestCase, and then use assertions like $this->expectOutputString('foo') and $this -> expectOutputRegex($regex) to verify that the output contains the string you're looking for. That technique works for simple cases, but if the method you want to test takes complex parameters or generates a lot of code, it's just not enough.
 +
 +
This example is based on /unittest/tests/administrator/components/com_modules/HTML_modules-0000-add-test.php. You will need to refer to the full test and understand [[Unit_Testing_Mock_Objects|mock objects]] in order to get a complete understanding of what is involved.
  
 
=== Capturing Output ===
 
=== Capturing Output ===
This snippet from a test demonstrates the method for capturing data.
+
This snippet from the test demonstrates the method for capturing data.
 
<source lang="php">
 
<source lang="php">
 
ob_start();
 
ob_start();
Line 11: Line 13:
 
$result = ob_get_flush();
 
$result = ob_get_flush();
 
</source>
 
</source>
The $result now variable has everything that would normally go to the browser.
+
The $result variable now has everything that would normally go to the browser. Note that PHP in command line mode may also dump all the output to the console. This is irritating but harmless.
  
 
=== Verifying the Output ===
 
=== Verifying the Output ===
Now that we have the output, how do we make sure it is "correct"?
+
Now that we have captured the output, how do we make sure it is "correct"?
  
 
In this example, there are two defining characteristics to the output: each module should be rendered as a radio button and a link. Let's look for the radio buttons first.
 
In this example, there are two defining characteristics to the output: each module should be rendered as a radio button and a link. Let's look for the radio buttons first.
 
<source lang="php">
 
<source lang="php">
// Get contents of all radio buttons
+
// Get contents of all radio buttons
preg_match_all('|<input .*type="radio".*>|U', $result, $matches);
+
preg_match_all('|<input .*type="radio".*>|U', $result, $matches);
// Filter for name=module
+
// Filter for name=module
foreach ($matches[0] as $index => $subs) {
+
foreach ($matches[0] as $index => $subs) {
if (strpos($subs, 'name="module"') === false) {
+
    if (strpos($subs, 'name="module"') === false) {
unset($matches[0][$index]);
+
        unset($matches[0][$index]);
}
+
    }
}
+
}
$this -> assertEquals(count($modules), count($matches[0]), 'Number of radio buttons.');
+
$this -> assertEquals(count($modules), count($matches[0]), 'Number of radio buttons.');
 
</source>
 
</source>
 +
If the number of radio buttons equals the number of modules, we pass the test.
 +
 +
For the links, we're going to get a little more complex. We're going to verify that each module we passed has precisely one link, and that the module reference matches the link description. To facilitate this, we start each module name and title with the same unique letter. Module "Amod" corresponds to name "Atest"; "Bmod" corresponds to "Btest" and so on. All we need to verify a match is to compare the first characters.
 +
<source lang="php">
 +
// Get contents of all links
 +
if (! preg_match_all('|<a .*module=(.*)&.*>(.*)</a>|U', $result, $matches)) {
 +
    $this -> fail('No links found.');
 +
}
 +
$this -> assertEquals(count($modules), count($matches[0]), 'Number of links.');
 +
for ($ind = 0; $ind < count($matches[0]); ++$ind) {
 +
    $this -> assertEquals(
 +
        $matches[1][$ind][0],
 +
        $matches[2][$ind][0],
 +
        $matches[1][$ind][0] . ' does not match ' . $matches[2][$ind][0]
 +
    );
 +
    $found = false;
 +
    foreach ($modules as $key => $item) {
 +
        if ($item -> module == $matches[1][$ind]) {
 +
            unset($modules[$key]);
 +
            $found = true;
 +
            break;
 +
        }
 +
    }
 +
    if (! $found) {
 +
        $this -> fail('Duplicate module in output ' . $matches[1][$ind]);
 +
    }
 +
}
 +
$this -> assertEquals(0, count($modules), 'Missing modules.');
 +
</source>
 +
 +
=== Validating the HTML ===
 +
So far our tests have proved that all the information we want appears in the output. But is the HTML valid?
 +
 +
If we install the Tidy extension, which is part of PHP but is normally not enabled, we can find out. since we're only generating part of a page, we need a utility function to put the output in the correct context.
 +
<source lang="php">
 +
function htmlWrap($html) {
 +
    $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
 +
        . ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' . chr(10)
 +
        . '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-gb" lang="en-gb">' . chr(10)
 +
        . '<head><title>hello</title></head>' . chr(10)
 +
        . '<body>' . chr(10)
 +
        . $html . chr(10)
 +
        . '</body></html>' . chr(10);
 +
    return $html;
 +
}
 +
</source>
 +
 +
Now we pass our captured output to be wrapped, then call Tidy.
 +
<source lang="php">
 +
$tidy = tidy_parse_string($this -> htmlWrap($result));
 +
$this -> assertEquals(0, tidy_error_count($tidy), 'Tidy errors ' . $tidy -> errorBuffer);
 +
$this -> assertEquals(4, tidy_warning_count($tidy), 'Tidy warnings ' . $tidy -> errorBuffer);
 +
</source>
 +
In this case, our code generates a couple of expected warnings. There is a tool tip class that uses a <span> element with the non-standard "name" and "value" attributes. Therefore we expect two errors per entry. Any more or less and we may have a problem.
 +
 +
== The Advantages of Unit Tests for UI Code ==
 +
* Building tests for UI code is easier than it seems at first.
 +
* Isolating a test significantly shortens the test cycle. A unit test eliminates the need to set up a test environment and repeatedly navigate through the same sequence just to get a simple segment of output.
 +
* The difficulty is in determining what part of the output can be used to verify that the test subject is functioning correctly, especially when there can be minor variations in the HTML that don't affect the correctness of the output.
 +
* As with other types of unit test, the value of the test increases with the number of edge cases that get tested. In this example, we run the tests with 0, 1, 2, and 3 modules defined. This successfully exercises all of the logic in the subject method.
 +
 +
Return to [[Unit_Testing|Unit Testing]]
 +
 +
[[Category:Testing]][[Category:Bug Squad]]

Revision as of 13:54, 15 December 2011

Contents

Testing User Interface Code

This expands on the basics given in the Simple Example. In this example, we want to test a class that outputs code to the browser.

PHPUnit has an extension to support this, but it's pretty limited. You can extend your test class from PHPUnit_Extensions_OutputTestCase, and then use assertions like $this->expectOutputString('foo') and $this -> expectOutputRegex($regex) to verify that the output contains the string you're looking for. That technique works for simple cases, but if the method you want to test takes complex parameters or generates a lot of code, it's just not enough.

This example is based on /unittest/tests/administrator/components/com_modules/HTML_modules-0000-add-test.php. You will need to refer to the full test and understand mock objects in order to get a complete understanding of what is involved.

Capturing Output

This snippet from the test demonstrates the method for capturing data.

ob_start();
HTML_modules::add($modules, $this -> client);
$result = ob_get_flush();

The $result variable now has everything that would normally go to the browser. Note that PHP in command line mode may also dump all the output to the console. This is irritating but harmless.

Verifying the Output

Now that we have captured the output, how do we make sure it is "correct"?

In this example, there are two defining characteristics to the output: each module should be rendered as a radio button and a link. Let's look for the radio buttons first.

// Get contents of all radio buttons
preg_match_all('|<input .*type="radio".*>|U', $result, $matches);
// Filter for name=module
foreach ($matches[0] as $index => $subs) {
    if (strpos($subs, 'name="module"') === false) {
        unset($matches[0][$index]);
    }
}
$this -> assertEquals(count($modules), count($matches[0]), 'Number of radio buttons.');

If the number of radio buttons equals the number of modules, we pass the test.

For the links, we're going to get a little more complex. We're going to verify that each module we passed has precisely one link, and that the module reference matches the link description. To facilitate this, we start each module name and title with the same unique letter. Module "Amod" corresponds to name "Atest"; "Bmod" corresponds to "Btest" and so on. All we need to verify a match is to compare the first characters.

// Get contents of all links
if (! preg_match_all('|<a .*module=(.*)&.*>(.*)</a>|U', $result, $matches)) {
    $this -> fail('No links found.');
}
$this -> assertEquals(count($modules), count($matches[0]), 'Number of links.');
for ($ind = 0; $ind < count($matches[0]); ++$ind) {
    $this -> assertEquals(
        $matches[1][$ind][0],
        $matches[2][$ind][0],
        $matches[1][$ind][0] . ' does not match ' . $matches[2][$ind][0]
    );
    $found = false;
    foreach ($modules as $key => $item) {
        if ($item -> module == $matches[1][$ind]) {
            unset($modules[$key]);
            $found = true;
            break;
        }
    }
    if (! $found) {
        $this -> fail('Duplicate module in output ' . $matches[1][$ind]);
    }
}
$this -> assertEquals(0, count($modules), 'Missing modules.');

Validating the HTML

So far our tests have proved that all the information we want appears in the output. But is the HTML valid?

If we install the Tidy extension, which is part of PHP but is normally not enabled, we can find out. since we're only generating part of a page, we need a utility function to put the output in the correct context.

function htmlWrap($html) {
    $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
        . ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' . chr(10)
        . '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-gb" lang="en-gb">' . chr(10)
        . '<head><title>hello</title></head>' . chr(10)
        . '<body>' . chr(10)
        . $html . chr(10)
        . '</body></html>' . chr(10);
    return $html;
}

Now we pass our captured output to be wrapped, then call Tidy.

$tidy = tidy_parse_string($this -> htmlWrap($result));
$this -> assertEquals(0, tidy_error_count($tidy), 'Tidy errors ' . $tidy -> errorBuffer);
$this -> assertEquals(4, tidy_warning_count($tidy), 'Tidy warnings ' . $tidy -> errorBuffer);

In this case, our code generates a couple of expected warnings. There is a tool tip class that uses a element with the non-standard "name" and "value" attributes. Therefore we expect two errors per entry. Any more or less and we may have a problem.

The Advantages of Unit Tests for UI Code

  • Building tests for UI code is easier than it seems at first.
  • Isolating a test significantly shortens the test cycle. A unit test eliminates the need to set up a test environment and repeatedly navigate through the same sequence just to get a simple segment of output.
  • The difficulty is in determining what part of the output can be used to verify that the test subject is functioning correctly, especially when there can be minor variations in the HTML that don't affect the correctness of the output.
  • As with other types of unit test, the value of the test increases with the number of edge cases that get tested. In this example, we run the tests with 0, 1, 2, and 3 modules defined. This successfully exercises all of the logic in the subject method.

Return to Unit Testing