0

I want to start with PHPSpec, so I'm working with two simple classes. The first one is responsible for applying a percentage reduce or enlarge to number, and the second one is responsible for calculate a product price with use of percentage applyer (like a Mock).

PercentageToNumberApplyerSpec (spec):

namespace spec\My;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class PercentageToNumberApplyerSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('My\PercentageToNumberApplyer');
    }

    function it_enlarges_a_number_with_a_given_percentage()
    {
        $this->enlarge(100, 20)->shouldReturn(120);
        $this->enlarge(80, 25)->shouldReturn(100);
        $this->enlarge(20, 50)->shouldReturn(30);
    }

    function it_reduces_a_number_with_a_given_percentage()
    {
        $this->reduce(100, 20)->shouldReturn(80);
        $this->reduce(80, 10)->shouldReturn(72);
        $this->reduce(250, 20)->shouldReturn(200);
    }
}

PercentageToNumberApplyer (implementation):

<?php

namespace My;

class PercentageToNumberApplyer
{
    /**
     * Enlarge given number with a given percentage
     *
     * @param $number
     * @param $percentage
     * @return float
     */
    public function enlarge($number, $percentage)
    {
        return $this->calculate($number, $percentage) + $number;
    }

    /**
     * Reduce given number with a given percentage
     *
     * @param $number
     * @param $percentage
     * @return mixed
     */
    public function reduce($number, $percentage)
    {
        return $number - $this->calculate($number, $percentage);
    }

    /**
     * @param $number
     * @param $percentage
     * @return float
     */
    private function calculate($number, $percentage)
    {
        return $number * $percentage / 100;
    }
}

PriceCalculatorSpec (spec):

<?php

namespace spec\My;

use My\PercentageToNumberApplyer;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class PriceCalculatorSpec extends ObjectBehavior
{
    function let(PercentageToNumberApplyer $percentageToNumberApplyer)
    {
        $this->beConstructedWith($percentageToNumberApplyer);
    }

    function it_calculates_price_discount($percentageToNumberApplyer)
    {
        $number = 100;
        $discount = 20;

        $percentageToNumberApplyer->reduce($number, $discount)->shouldBeCalled();

        $this->applyDiscountTo($number, $discount)->shouldReturn(80);
    }
}

The Problem

The problem is that in the above example after running phpspec run the result is:

- it calculates price discount
expected [integer:80], but got null

PriceCalculator (implementation):

<?php

namespace My;

class PriceCalculator
{
    /**
     * @var PercentageToNumberApplyer
     */
    private $percentageToNumberApplyer;

    /**
     * @param PercentageToNumberApplyer $percentageToNumberApplyer
     */
    public function __construct(PercentageToNumberApplyer $percentageToNumberApplyer)
    {
        $this->percentageToNumberApplyer = $percentageToNumberApplyer;
    }

    /**
     * @param $basePrice
     * @param $discount
     * @return mixed
     */
    public function applyDiscountTo($basePrice, $discount)
    {
        return $this->percentageToNumberApplyer->reduce($basePrice, $discount);
    }
}

Why the following use case is working, even if the test fails:

$priceCalculator = new \My\PriceCalculator(new \My\PercentageToNumberApplyer());

$price = $priceCalculator->applyDiscountTo(100, 20);

$price have a 80 value...

Karol F
  • 1,556
  • 2
  • 18
  • 33

1 Answers1

1

You don't need a mock in your case. Stub will do. Read more on test doubles in PHP Test doubles patterns with prophecy.

Instead of mocking a call:

$percentageToNumberApplyer->reduce($number, $discount)->shouldBeCalled();

Stub it:

$percentageToNumberApplyer->reduce($number, $discount)->willReturn(80);

Next, you only need to expect that what's calculated is actually returned:

$percentageToNumberApplyer->reduce($number, $discount)->willReturn(80);

$this->applyDiscountTo($number, $discount)->shouldReturn(80);

This is because you shouldn't care if a call was made. You're only interested in the result.

As a rule of thumb, you'll usually:

  • mock collaborators that perform an action (command methods)
  • stub collaborators that return something (query methods)

Most of the time it's better to have a separation between the two (Command/Query Separation), and we won't need to mock and stub in the same time.

Jakub Zalas
  • 35,761
  • 9
  • 93
  • 125
  • Thank you again Jakub :), but If I do: $percentageToNumberApplyer->reduce(100, 20)->willReturn(100) which is not true the test pass, so every time I have to think about writing "true" am I right? – Karol F Mar 19 '15 at 08:42
  • It doesn't matter from this spec's perspective what you put here. The intent is that whatever the collaborator returns is returned here. The class that actually makes the calculation is speced on its own. If there was a bug in calculation and you haven't mocked the collaborator that makes the calculation, you'd have to tests failing. If you mock the collaborator, the only failing test will be the one were you actually verify the calculation. It's a better feedback. – Jakub Zalas Mar 19 '15 at 11:37