Rafał Łyczkowski
Rafał Łyczkowski

Reputation: 1015

How to do a prophecy for current testing class in PHPUnit?

I have this case that I want to run PHPUnit test and check behaviour of current testing class as follows:

public function it_allows_to_add_items()
{
        // Create prophesies
        $managerProphecy = $this->getProphet(ListingManager::class);
        $listingItemProphecy = $this->getProphet(ListingItemInterface::class);

        $listing = factory(\App\Misc\Listings\Listing::class)->create();
        $manager = new ListingManager($listing);

        $item = factory(\App\Misc\Listings\ListingItem::class)->make(['listing_id' => null]);
        $item2 = factory(\App\Misc\Listings\ListingItem::class)->make(['listing_id' => null]);

        $manager->addItem($item);
        $managerProphecy->validate($listingItemProphecy)->shouldBeCalledTimes(2);
        $manager->addItem($item2);

        $this->assertTrue(true);
    }

is that even possible ?

Of course I'm getting

1) GenericListingManagerTest::it_allows_to_add_items
Some predictions failed:
  Double\App\Misc\Listings\ListingManager\P2:
    Expected exactly 2 calls that match:
      Double\App\Misc\Listings\ListingManager\P2->validate(exact(Double\ListingItemInterface\P1:00000000058d2b7a00007feda4ff3b5f Object (
        'objectProphecy' => Prophecy\Prophecy\ObjectProphecy Object (*Prophecy*)
    )))
    but none were made.

Upvotes: 0

Views: 1255

Answers (1)

dbrumann
dbrumann

Reputation: 17166

I think your way to approach this test is a bit off. If I understand correctly you want to verify that addItem(object $item) works properly, i.e. the manager contains the item and the item is the same you added. For this you should not need prophecies and in fact your test does not actually use the prophecies you created. Depending on what your Manager looks like you could write something like this:

function test_manager_add_items_stores_item_and_increases_count()
{
    $manager = new ListingManager(); // (2)
    $item = new ListingIem(); // (3)
    $initialCount = $manager->countItems(); // (1)

    $manager->addItem($item);

    $this->assertEquals($initialCount + 1, $manager->countItems());
    // Assuming offset equals (item count - 1) just like in a numeric array
    $this->assertSame($item, $manager->getItemAtOffset($initialCount));
}

(1) Assuming your manager has a count()-method you can check before and after whether the count increased by the number of added items.

(2) We want to test the real manager - that's why we don't need a mock created by prophecy here - and we can use a real item because it's just a value.

(3) I'm not sure why you have a ListingItemInterface. Are there really different implementations of ListingItem and if so, do you really want a generic ListingManager that possibly contains all of them or do you need a more specific one to make sure that each Manager contains only it's specific kind of item? This really depends on your use case, but it looks like you might violate the I (Interface Segregation Principle) or L (Liskov Substitution Principle) in SOLID.

Depending on your use case you might want to add real items, e.g. 2 different types to make clear that it's intended that you can put 2 different implementations of the interface in there or you can do it like above and just add a ListingItem and verify that each item in the manager implements the interface - I leave finding the assertion for that to you ;). Of course you can use your factory to create the item as well. The important thing is that we test with assertSame() whether the managed object and the one we created initially are the same, meaning reference the exact same object.

You could add additional tests if you want to ensure additional behaviour, such as restricting the kind of items you can put in the manager or how it behaves when you put an invalid object in there.

The important thing is, that you want to test the actual behaviour of the Manager and that's why you don't want to use a mock for it. You could use a mock for the ListingItemInterface should you really require it. In that case the test should probably look something like this:

function test_manager_add_items_stores_item_and_increases_count()
{
    $manager = new ListingManager();
    $dummyItem = $this->prophecy(ListingIemInterface::class);
    $initialCount = $manager->countItems();

    $manager->addItem($dummyItem->reveal());

    $this->assertEquals($initialCount + 1, $manager->countItems());
    $this->assertSame($dummyItem, $manager->getItemAtOffset($initialCount));
}

edit: If addItem checks the validation which you want to skip, e.g. because the empty item you provide is not valid and you don't care. You can use PHPUnit's own mock framework to partially mock the manager like this:

$item = new ListingItem();
$managerMock = $this->getMockBuilder(ListManager::class)
    ->setMethods(['validate'])
    ->getMock();
$managerMock
    ->expects($this->exactly(2))
    ->method('validate')
    ->with($this-> identicalTo($item))
    ->willReturn(true);

$managerMock->addItem($item);
$managerMock->addItem($item);

You don't have to assert anything at the end, because expects() is already asserting something. Your Manager will work normally, except for validate(), meaning you will run the code in addItem() and if in there validate is called (once per item) the test will pass.

Upvotes: 1

Related Questions