My shopping cart application is coming right along. I can add items and the total price seems to be accurate. However, while I was playing around with my new cart I noticed a very strange problem. I was playing around with the idea of allowing discounts to be applied to a cart as just additional items that would have a negative price. So while I am playing around with this idea I notice that the math isn't always adding up. If I start with an item that is $100 and then add a discount that is $81.40 I see that the total price isn't adding up to $18.60. This is definitely problematic After doing some further research, I realize I made a silly mistake. I am just using simple floats to calculate the costs. Floats are by nature inacurrate. Once you start using them in mathematical operations they start to show their inadequacy for precision. In keeping with the test driven method of creating code I need to create a unit test that shows this flaw.
Example 2.6. ShoppingCartTest
<?php class ShoppingCartTest extends PHPUnit_Framework_TestCase { private $shoppingCart; private $item1; private $item2; private $item3; public function setUp() { $this->item1 = Phake::mock('Item'); $this->item2 = Phake::mock('Item'); $this->item3 = Phake::mock('Item'); Phake::when($this->item1)->getPrice()->thenReturn(100); Phake::when($this->item2)->getPrice()->thenReturn(200); Phake::when($this->item3)->getPrice()->thenReturn(300); $this->shoppingCart = new ShoppingCart(); $this->shoppingCart->addItem($this->item1); $this->shoppingCart->addItem($this->item2); $this->shoppingCart->addItem($this->item3); } public function testGetSub() { $this->assertEquals(600, $this->shoppingCart->getSubTotal()); } public function testGetSubTotalWithPrecision() { $this->item1 = Phake::mock('Item'); $this->item2 = Phake::mock('Item'); $this->item3 = Phake::mock('Item'); Phake::when($this->item1)->getPrice()->thenReturn(100); Phake::when($this->item2)->getPrice()->thenReturn(-81.4); Phake::when($this->item3)->getPrice()->thenReturn(20); $this->shoppingCart = new ShoppingCart(); $this->shoppingCart->addItem($this->item1); $this->shoppingCart->addItem($this->item2); $this->shoppingCart->addItem($this->item3); $this->assertEquals(38.6, $this->shoppingCart->getSubTotal()); } } ?>
You can see that I added another test method that uses actual floats for some of the prices as opposed to round numbers. Now when I run my test sweet I can see the fantastic floating point issue.
Example 2.7. ShoppingCartTest Output
$ phpunit ExampleTests/ShoppingCartTest.php PHPUnit 3.5.13 by Sebastian Bergmann. .F Time: 0 seconds, Memory: 10.25Mb There was 1 failure: 1) ShoppingCartTest::testGetSubTotalWithPrecision Failed asserting that <double:38.6> matches expected <double:38.6>. /home/mikel/Documents/Projects/Phake/tests/ShoppingCartTest.php:95 FAILURES! Tests: 2, Assertions: 2, Failures: 1. Generating code coverage report, this may take a moment.
Once you get over the strangeness of 38.6 not equaling 38.6 I want to discuss streamlining test cases with you. You
will notice that the code in ShoppingCartTest::testGetSubTotalWithPrecision() contains almost
all duplicate code when compared to ShoppingCartTest::setUp(). If I were to continue following
this pattern of doing things I would eventually have tests that are difficult to maintain. Phake allows you to very
easily override stubs. This is very important in helping you to reduce duplication in your tests and leads to tests
that will be easier to maintain. To overwrite a previous stub you simply have to redefine it. I am going to change
ShoppingCartTest::testGetSubTotalWithPrecision() to instead just redefine the getPrice()
stubs.
Example 2.8. ShoppingCartTest
<?php class ShoppingCartTest extends PHPUnit_Framework_TestCase { private $shoppingCart; private $item1; private $item2; private $item3; public function setUp() { $this->item1 = Phake::mock('Item'); $this->item2 = Phake::mock('Item'); $this->item3 = Phake::mock('Item'); Phake::when($this->item1)->getPrice()->thenReturn(100); Phake::when($this->item2)->getPrice()->thenReturn(200); Phake::when($this->item3)->getPrice()->thenReturn(300); $this->shoppingCart = new ShoppingCart(); $this->shoppingCart->addItem($this->item1); $this->shoppingCart->addItem($this->item2); $this->shoppingCart->addItem($this->item3); } public function testGetSub() { $this->assertEquals(600, $this->shoppingCart->getSubTotal()); } public function testGetSubTotalWithPrecision() { Phake::when($this->item1)->getPrice()->thenReturn(100); Phake::when($this->item2)->getPrice()->thenReturn(-81.4); Phake::when($this->item3)->getPrice()->thenReturn(20); $this->assertEquals(38.6, $this->shoppingCart->getSubTotal()); } } ?>
If you rerun this test you will get the same results shown in Example 2.7, “ShoppingCartTest Output”.
The test itself is much simpler though there is much less unnecessary duplication. The reason this works is because
the stub map I was referring to in the section called “How Phake::when() Works” isn't really a map at all. It is more of
a stack in reality. When a new matcher and answer pair is added to a mock object, it is added to the top of the stack.
Then whenever a stub method is called, the stack is checked from the top down to find the first matcher that matches
the method that was called. So, when I created the additional stubs for the various Item::getPrice()
calls, I was just adding additional matchers to the top of the stack that would always get matched first by virtue
of the parameters all being the same.