Testing: Writing a Test in PHP

The restaurant_check() function from Example 5-11 calculates the total bill for a restaurant meal, given the cost of the meal itself, the tax rate, and the tip rate. Example 13-3 shows the function again to refresh your memory.

Example 13-3. restaurant_check()

function restaurant_check($meal, $tax, $tip) {

$tax_amount = $meal * ($tax / 10 );

$tip_amount = $meal * ($tip / 10 );

$total_amount = $meal + $tax_amount + $tip_amount;

}

Tests in PHPUnit are organized as methods inside a class. The class you write to con­tain your tests must extend the PHPUnit_Framework_TestCase class. The name of each method that implements a test must begin with test. Example 13-4 shows a class with a test in it for restaurant_check().

Example 13-4. Testing restaurant check calculation

include ‘restaurant-check.php’;

class RestaurantCheckTest extends PHPUnit_Framework_TestCase {

public function testWithTaxAndTip() {

$meal = 100;

$tax = 10;

$tip = 20;

$result = restaurant_check($meal, $tax, $tip);

$this->assertEquals(130, $result);

}

}

Note that Example 13-4 assumes that the restaurant_check() function is defined in a file named restaurant-check.php, which it includes before defining the test class. It is your responsibility to make sure that the code that your tests are testing is loaded and available for your test class to invoke.

To run the test, give the filename you’ve saved the code in as an argument to the PHPUnit program:

phpunit.phar RestaurantCheckTest.php

That produces output like the following:

PHPUnit 4.8.11 by Sebastian Bergmann and contributors.

.

Time: 121 ms, Memory: 13.50Mb

OK (1 test, 1 assertion)

Each . before the Time: line represents one test that was run. The last line (OK (1 test, 1 assertion)) tells you the status of all the tests, how many tests were run, and how many assertions all those tests contained. An OK status means no tests failed. This example had one test method, testWithTaxAndTip(), and inside that test method there was one assertion: the call to assertEquals() that checked that the return value from the function equaled 130.

A test method is generally structured like the preceding example. It has a name begin­ning with test that describes what behavior the method is testing. It does any vari­able initialization or setup necessary to exercise the code to test. It invokes the code to test. Then it makes some assertions about what happened. Assertions are available as instance methods on the PHPUnit_Framework_TestCase class, so they are available in our test subclass.

The assertion method names each begin with assert. These methods let you check all sorts of aspects of how your code works, such as whether values are equal, ele­ments are present in an array, or an object is an instance of a certain class. Appendix A of the PHPUnit manual lists all the assertion methods available.

PHPUnit’s output looks different when a test fails. Example 13-5 adds a second test method to the RestaurantCheckTest class.

Example 13-5. A test with a failing assertion

include ‘restaurant-check.php’;

class RestaurantCheckTest extends PHPUnit_Framework_TestCase

{

public function testWithTaxAndTip() {

$meal = 100;

$tax = 10;

$tip = 20;

$result = restaurant_check($meal, $tax, $tip);

$this->assertEquals(130, $result);

}

public function testWithNoTip() {

$meal = 100;

$tax = 10;

$tip = 0;

$result = restaurant_check($meal, $tax, $tip);

$this->assertEquals(120, $result);

}

}

In Example 13-5, the testWithNoTip() test method asserts that the total check on a $100 meal with 10% tax and no tip should equal $120. This is wrong—the total should be $110. PHPUnit’s output in this case looks like this:

PHPUnit 4.8.11 by Sebastian Bergmann and contributors.

.F

Time: 129 ms, Memory: 13.50Mb

There was 1 failure:

1) RestaurantCheckTest::testWithNoTip

Failed asserting that 110.0 matches expected 120.

RestaurantCheckTest.php:20

FAILURES!

Tests: 2, Assertions: 2, Failures: 1.

Because the test fails, it gets an F instead of a . in the initial part of the output. PHPUnit also reports more details on the failure. It tells you what test class and test method contained the failure, and what the failed assertion was. The test code expected 120 (the first argument to assertEquals()) but instead got 110 (the second argument to assertEquals()).

If you change the assertion in testWithNoTipO to expect 110 instead, the test passes.

Some deliberation and creativity is usually required to ensure that your tests cover an adequate variety of situations so that you have confidence in how your code behaves. For example, how should restaurant_check() calculate the tip? Some people calcu­late the tip just on the meal amount, and some on the meal amount plus tax. A test is a good way to be explicit about your function’s behavior. Example 13-6 adds tests that verify the function’s existing behavior: the tip is calculated only on the meal, not on the tax.

Example 13-6. Testing how tip is calculated

include ‘restaurant-check.php’;

class RestaurantCheckTest extends PHPUnit_Framework_TestCase

{

public function testWithTaxAndTip() {

$meal = 100;

$tax = 10;

$tip = 20;

$result = restaurant_check($meal, $tax, $tip);

$this->assertEquals(130, $result);

}

public function testWithNoTip() {

$meal = 100;

$tax = 10;

$tip = 0;

$result = restaurant_check($meal, $tax, $tip);

$this->assertEquals(110, $result);

}

public function testTipIsNotOnTax() {

$meal = 100;

$tax = 10;

$tip = 10;

$checkWithTax = restaurant_check($meal, $tax, $tip);

$checkWithoutTax = restaurant_check($meal, 0 , $tip);

$expectedTax = $meal * ($tax / 100);

$this->assertEquals($checkWithTax, $checkWithoutTax + $expectedTax);

}

}

The testTipIsNotOnTax() method calculates two different restaurant checks: one with the provided tax rate and one with a tax rate of 0. The difference between these two should just be the expected amount of tax. There should not also be a difference in the tip. The assertion in this test method checks that the check with tax is equal to the check without tax, plus the expected tax amount. This ensures that the function is not calculating the tip on the tax amount, too.

Source: Sklar David (2016), Learning PHP: A Gentle Introduction to the Web’s Most Popular Language, O’Reilly Media; 1st edition.

Leave a Reply

Your email address will not be published. Required fields are marked *