Search code examples
phptestingtddphpspec

PhpSpec - check method returns an integer rather than "willReturn(x)"


Is there a way that I can test that the getNumberOfProductsForCategory method will return an integer rather than a specific value (18 in this case)?

I feel simply testing for "18", although correct just now, makes my test very brittle as that value will likely change at some point in the future, meaning the test will fail.

This is my PhpSpec function (reduced for brevity):

    function it_returns_something_if_products_exist(
       ProductRepository $productRepository, 
       Category $category
    )
    {
       $category->getId()->willReturn(268);
       $productRepository->getNumberOfProductsForCategory($category)->willReturn(18);

       // call method here
    }

This is my repository method:

/**
 * @param Category $category
 *
 * @return mixed
 */
public function getNumberOfProductsForCategory(Category $category)
{
    $dql = 'SELECT COUNT(tvp)
                FROM \CRMPicco\ProductBundle\Entity\Product tvp
            WHERE tvp.category = :category';

    return $this->getEntityManager()
        ->createQuery($dql)
        ->setParameter('category', $category)
        ->getSingleScalarResult();
}

Solution

  • You are not testing anything in the posted code. $productRepository and $category are stubs and the calls you do configure their behaviour.

    For example,

    $category->getId()->willReturn(268);
    

    configures the object $category (which mimics the behaviour of an object of Category type) to return 268 when its method getId() is invoked.

    Without this configuration, $category->getId() returns NULL.

    In fact, phpspec is not a testing tool, it is a specification tool. It is not used to test the behaviour of the code but to describe its behaviour. Take a look of what the creator of phpspec says about the limitations of phpspec. Especially check the limitation #8:

    The thing is, PhpSpec was not designed for integration tests – in fact, it wasn’t designed for testing anything really. It was designed as a tool to help you come up with well-designed classes.

    it from the name of the function it_returns_blah_blah_blah() refers to the class you describe and that class is the class that provides the name of the specification. If, for example, the function you posted is a member of class FormatterSpec it means it describes the behaviour of class Formatter.

    The name of the method it_returns_something_if_products_exist() should suggest what functionality is described by it.

    Let's say you want to describe the method Formatter::formatNumberOfProducts(). It has two arguments (Category $category and ProductRepository $productRepository) and returns a string that is either 1 product or X products (replace X with the actual number of products in the category).

    The spec could look like this:

    class FormatterSpec
    {
        function it_formats_number_of_products_when_they_are_many(
           ProductRepository $productRepository, 
           Category $category
        )
        {
            // prepare the stubs
            $category->getId()->willReturn(268);
            $productRepository->getNumberOfProductsForCategory($category)
                ->willReturn(18);
    
            // describe the behaviour
            $this->formatNumberOfProducts($category, $productRepository)
                ->shouldReturnString();
            $this->formatNumberOfProducts($category, $productRepository)
                ->shouldBe('18 products');
        }
    
    
        function it_formats_number_of_products_when_there_is_only_one(
           ProductRepository $productRepository, 
           Category $category
        )
        {
            // prepare the stubs
            $category->getId()->willReturn(268);
            $productRepository->getNumberOfProductsForCategory($category)
                ->willReturn(1);
    
            // describe the behaviour
            $this->formatNumberOfProducts($category, $productRepository)
                ->shouldReturnString();
            $this->formatNumberOfProducts($category, $productRepository)
                ->shouldBe('1 product');
        }
    }
    

    You need to configure the stubs because you know the method formatNumberOfProducts() calls some of their methods. You use stubs because you don't care how $productRepository gets its data; all you care is that it returns a number and the described method must use that number in a specific way.