phpspec / prophecy

Highly opinionated mocking framework for PHP 5.3+
MIT License
8.53k stars 241 forks source link

Cannot handle DateTime dummies #454

Closed elvetemedve closed 2 months ago

elvetemedve commented 4 years ago

Summary

When you want to use \DateTime object in PHPSpec tests as dummy object, then the test fails with the following error message:

err:Error("Call to a member function sub() on null")

The Problem

Let's suppose we have two classes Subject and Collaborator and we are writing spec for the former. Source code looks like this:

<?php
// spec/SubjectSpec.php

namespace spec;

use PhpSpec\ObjectBehavior;
use Subject;

class SubjectSpec extends ObjectBehavior
{
    function it_can_handle_date_time_dummies(\DateTime $date, \Collaborator $c)
    {
    $this->beConstructedWith($c);

    $c->takeADate($date)->willReturn(true);

        $this->doSomething($date)->shouldReturn(true);
    }
}
<?php
// src/Subject.php

class Subject
{
    private $c;

    public function __construct(Collaborator $c)
    {
        $this->c = $c;
    }

    public function doSomething(\DateTime $date)
    {
        return $this->c->takeADate($date);
    }
}
<?php
// src/Collaborator.php

interface Collaborator
{
    public function takeADate(\DateTime $date);
}

composer.json

{
    "name": "gbuza/spec-test",
    "authors": [
        {
            "name": "Géza Búza",
            "email": "bghome@gmail.com"
        }
    ],
    "require": {
        "phpspec/phpspec": "^6.1"
    },
    "autoload": {
        "psr-0": {
            "": "src"
        }
    }
}

Finally execute the tests: vendor/bin/phpspec run

Actual Result

Subject                                                                         
  10  - it can handle date time dummies
      exception [err:Error("Call to a member function sub() on null")] has been thrown.

                                      100%                                       1
1 specs
1 example (1 broken)

Expected Result

1 specs
1 example (1 passed)

Explanation

Prophecy uses the DateTimeComparator class from sebastian/comparator package from the ExactValueToken class in order to compare the actual date argument with the argument of the promise object. That worked fine before 2.1.1 or earlier versions. But in 2.1.2 methods of the \DateTime class are chained like $expected->setTimezone()->sub(). Since $expected is not a real \DateTime object, but actually a dummy test double, all of the methods will return null by definition. Therefore $expected->setTimezone() becomes null, that's why sub() cannot be called on that.

Since our compared object is not a real \DateTime, it should not be compared by the DateTimeComparator. The condition to support an object is to check the inheritance chain, it will be accepted for comparison which is fine when the real \DateTime class is extended.

stof commented 2 months ago

As DateTime is a value object, I would suggest using an actual one instead of mocking it (and otherwise, you have to configure the mock to actually behave like the PHP class, which is your responsibility if you create a mock for it, which is the main reason for the common saying "don't mock what you don't own")

stof commented 2 months ago

This is not something that can be fixed in prophecy as prophecy cannot autoconfigure a mock to behave like the actual class (as it does not know how it is supposed to behave).