Stubbing a method called by a class' constructor

前端 未结 1 905
执念已碎
执念已碎 2021-02-12 08:38

How does one stub a method in PHPUnit that is called by the class under test\'s constructor? The simple code below for example won\'t work because by the time I declare the stub

1条回答
  •  情深已故
    2021-02-12 09:28

    The problem is not the stubbing of the method, but your class.

    You are doing work in the constructor. In order to set the object into state, you fetch a remote file. But that step is not necessary, because the object doesn't need that data to be in a valid state. You dont need the result from the file before you actually call getFormatted.

    You could defer loading:

    class ClassA {
      private $dog;
      private $formatted;
    
      public function __construct($param1) { 
         $this->dog = $param1;       
      }
      protected getResultFromRemoteServer() {
        if (!$this->formatted) {
            $this->formatted = file_get_contents(
                'http://whatever.com/index.php?' . $this->dog
            );
        }
        return $this->formatted;
      }
      public getFormatted() {
        return ("The dog is a " . $this->getResultFromRemoteServer());
      }
    }
    

    so you are lazy loading the remote access to when it's actually needed. Now you dont need to stub getResultFromRemoteServer at all, but can stub getFormatted instead. You also won't need to open your API for the testing and make getResultFromRemoteServer public then.

    On a sidenote, even if it's just an example, I would rewrite that class to read

    class DogFinder
    {
        protected $lookupUri;
        protected $cache = array();
        public function __construct($lookupUri)
        {
            $this->lookupUri = $lookupUri;
        }
        protected function findById($dog)
        {
            if (!isset($this->cache[$dog])) {
                $this->cache[$dog] = file_get_contents(
                    urlencode($this->lookupUri . $dog)
                );
            }
            return $this->cache[$id];
        }
        public function getFormatted($dog, $format = 'This is a %s')
        {
            return sprintf($format, $this->findById($dog));
        }
    }
    

    Since it's a Finder, it might make more sense to actually have findById public now. Just keeping it protected because that's what you had in your example.


    The other option would be to extend the Subject-Under-Test and replace the method getResultFromRemoteServer with your own implementation returning Poodle. This would mean you are not testing the actual ClassA, but a subclass of ClassA, but this is what happens when you use the Mock API anyway.

    As of PHP7, you could utilize an Anonymous class like this:

    public function testPoodle() {
    
        $stub = new class('dog52') extends ClassA {
          public function getResultFromRemoteServer() {
              return 'Poodle';
          }
        };
    
        $expected = 'This dog is a Poodle';
        $actual = $stub->getFormatted();
        $this->assertEquals($expected, $actual);
    }
    

    Before PHP7, you'd just write a regular class extending the Subject-Under-Test and use that instead of the Subject-Under-Test. Or use disableOriginalConstructor as shown elsewhere on this page.

    0 讨论(0)
提交回复
热议问题