phptestingtestng-dataproviderphpspec

How to properly test subclasses with phpspec dataprovider


I'm pretty new to Phpspec testing and I don't know what is the correct way to test multiple scenarios when transforming a object to different response structure.

I need to check if price is correctly calculated. Here I have the Transformer spec test:

/**
 * @dataProvider pricesProvider
 */
public function it_should_check_whether_the_prices_are_correct(
    $priceWithoutVat,
    $priceWithVat,
    $vat,
    Request $request,
    Repository $repository
) {
    $productIds = array(100001);

    $result = array(
        new Product(
            '100001',
            'MONSTER',
            new Price(
                $priceWithoutVat,
                20,
                'GBP',
                null,
                null
            )
        )
    );

    $expected = array(
        array(
            "productId" => "100001",
            "brand" => "MONSTER",
            "price" => array(
                "amount" => $priceWithVat,
                "vatAmount" => $vat,
                "currencyCode" => "GBP",
                "discountAmount" => (int)0
            )
        )
    );

    $repository->getResult(array(
        Repository::FILTER_IDS => $productIds
    ))->willReturn($result);

    $request->get('productIds')->willReturn(productIds);

    /**  @var SubjectSpec $transformedData */
    $transformedData = $this->transform($request);
    $transformedData->shouldEqual($expected);
}

public function pricesProvider()
{
    return array(
        array('123.456789', 14814, 2469),
        array('60.00', 7200, 1200),
    );
}

In my Transformer class I have a function which formats data to the correct format:

public function transform(Request $request)
{
    $productIds = $request->get('productIds');

    $productsResult = $this->repository->getResult(array(
        Repository::FILTER_IDS => $productIds
    ));

    $products = array();
    foreach ($productsResult as $product) {
        $products[] = $this->formatData($product);
    }

    return $products;
}

/**
 * @param Product $product
 * @return array
 */
private function formatData(Product $product)
{
    return array(
        'productId' => $product->getId(),
        'brand' => $product->getBrandName(),        
        'price' => array(
            'amount' => (int)bcmul($product->getPrice()->getAmountWithTax(), '100'),
            'vatAmount' => (int)bcmul($product->getPrice()->getTaxAmount(), '100'),
            'currencyCode' => $product->getPrice()->getCurrencyCode(),
            'discountAmount' => (int)bcmul($product->getPrice()->getDiscountAmount(), '100')
        )
    );
}

The problem is, that I'm getting this error message:

316  - it should check whether the prices are correct
  warning: bcmul() expects parameter 1 to be string, object given in
  /src/AppBundle/Database/Entity/Product/Price/Price.php line 49

If I hard-code those values then the test is green. However I want to test varios prices and results, so I decided to use the dataProvider method. But when dataProvider passes the $amountWithoutTax value, it's not string but PhpSpec\Wrapper\Collaborator class and because of this the bcmul fails.

If I change the $amountWithoutTax value to $priceWithoutVat->getWrappedObject() then Double\stdClass\P97 class is passed and because of this the bcmul fails.

How do I make this work? Is it some banality or did I completely misunderstood the concept of this?

I use https://github.com/coduo/phpspec-data-provider-extension and in composer.json have the following:

"require-dev": {
    "phpspec/phpspec": "2.5.8",
    "coduo/phpspec-data-provider-extension": "^1.0"
}

Solution

  • If getAmountWithTax() in your formatData method returns an instance of PhpSpec\Wrapper\Collaborator, it means that it returns a Prophecy mock builder instead of the actual mock, i.e. the one that you get by calling reveal() method. I don't know how your data provider looks like, but it seems that you're mocking your Price value objects instead of creating real instances thereof, and $product->getPrice() in your production code returns the wrong kind of object.

    The solution would be either to create a real instance of the Price value object that's later returned by $product->getPrice() with new in the data provider, or by calling reveal() on that instance, like this (assuming $price is a mock object that comes from a type hinted parameter):

    $product->getPrice()->willReturn($price->reveal());