PHPUnit test a method that returns an objects prop

2019-06-09 15:21发布

问题:

public function thisMethod {
    $example = $this->methodReturnsObject()->this1->that2->there3->id;
    return $example;
}

How would you test thisMethod in PHPUnit?

Obviously I could write an expectation that methodReturnsObject() will return something... but what? That object has properties associated with it, but how would you even mock that value?

回答1:

The answer is "You don't". Unit testing should test each class in isolation, what you are trying to do there is not a unit test. As I said in my comment, you are breaking the Law of Demeter, which simply stated says

  • Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
  • Each unit should only talk to its friends; don't talk to strangers.
  • Only talk to your immediate friends.

You have tightly coupled classes there that need re-factoring. I have written the classes first here to illustrate the point, but I usually write the tests first.

Lets start with the end of the chain:-

class there3
{
    private $id

    public function setId($id)
    {
        $this->id = $id;
    }

    public function getId()
    {
        return $this->id;
    }

}

Now let's set up a unit test for it:-

class there3Test extends PHPUnit_Framework_TestCase
{
    public function testCanGetId()
    {
        $there3 = new there3();
        $there3->setId(3);
        $this->assertTrue($there3->getId() === 3);
    }
}

That class is now tested, so we don't need to test it again. Now let's look at the next one:-

class this2
{
    public $there3;

    //To facilitate unit testing we inject the dependency so we can mock it
    public function __construct(there3 $there3)
    {
        $this->there3 = $there3;
    }

    public function getId()
    {
        return $this->there3->getId();
    }

}

And now the unit test:-

class this2Test extends PHPUnit_Framework_TestCase
{
    public function testCanGetId()
    {
        $mockThere3 = $this->getMock('there3');
        $mockThere3->method('getId')
                   ->will($this->returnValue(3);

        $this2 = new this2($mockThere3);//We pass in the mock object instead of the real one
        $this->assertTrue($this2->getId() === 3);
    }
}

We'll do one last example to further illustrate my point:-

class this1
{
    private $this2;

    public function __construct(this2 $this2)//injecting again
    {
         $this->$this2 = $this2;
    }

    public function getId()
    {
        return $this->$this2->getId();
    }
}

And, again, the unit test:-

class this1Test extends PHPUnit_Framework_TestCase
{
    public function testCanGetId()
    {
        $mockThis2 = $this->getMock('this2');
        $mockThis2->method('getId')
                  ->will($this->returnValue(3);

        $this1 = new this1($mockThis2);//We pass in the mock object instead of the real one
        $this->assertTrue($this1->getId() === 3);
    }
}

Hopefully, you get the idea without me having to go through all the objects in your example.

What I have done is to de-couple the classes from each other. They only have knowledge of the object they depend on, they don't care how that object gets the information requested.

Now the call for id would look something like:-

public function getId()
{
    return $this->this1->getId();
}

Which will go up the chain until the id returned is there2::id. You never have to write something like $this->$this1->$this2->there3->id and you can unit test your classes properly.

For more information on unit testing see the PHPUnit manual.



回答2:

How would you test thisMethod in PHPUnit, I'd either:

  1. try to avoid getting to a point where the unit test is mocking 3+ nested parts
  2. create a random version of the object, and verify the random is the same

Such as:

<?php

// an opaque known
$random = rand();

// the results
$value = new stdClass();
$value->this1 = new stdClass();
$value->this1->this2 = new stdClass();
$value->this1->this2->there3 = new stdClass();
$value->this1->this2->there3->id = $random;

// somehow get the known value into the fixture
// not enough detail shown about $this->methodReturnsObject()
// to make a reasonable suggestion about it.


$this->assertEquals(
   $random, $this->fixture->thisMethod(),
   "Got a value from somewhere else"
);


回答3:

Use rrehbein's method to build the return value from the mock method and create a partial mock of whatever class contains thisMethod and methodReturnsObject--the class under test. Mock methodReturnsObject to return the $value object created.

Assuming that class is named Foo gives

function testThisMethod() {
    $foo = $this->getMock('Foo', array('methodReturnsObject'));
    $value = // create as rrehbein demonstrated
    $foo->expects($this->once())
        ->method('methodReturnsObject')
        ->will($this->returnValue($value));
    self::assertSame($value, $foo->thisMethod());
}