freekmurze's avatar

How to test method chains with PhpSpec

Imagine your have this class:

<?php

class Elasticsearch {

    protected $indexName;


    public function __construct(Client $elasticsearch)
    {
        $this->elasticsearch = $elasticsearch;

    }


    /**
     * Set the name of the index that should be used by default
     *
     * @param $indexName
     * @return $this
     */
    public function setIndexName($indexName)
    {
        $this->indexName = $indexName;

        return $this;
    }


    ...

    /**
     * Remove everything from the index
     *
     * @return mixed
     */
    public function clearIndex()
    {
        $this->elasticsearch->indices()->delete(['index' => $this->indexName]);
    }

    ...
}

How would you go about testing the clearIndex in PhpSpec? I tried this test:

<?php

...

class ElasticsearchSpec extends ObjectBehavior
{

    protected $indexName;


    protected $searchableObject;

    public function __construct()
    {
        $this->indexName = 'indexName';

    }

    function let(Client $elasticsearch, Searchable $searchableObject)
    {
        $this->beConstructedWith($elasticsearch);

        $this->setIndexName($this->indexName);
    }

    function it_is_initializable()
    {
        $this->shouldHaveType('Spatie\SearchIndex\Elasticsearch\Elasticsearch');
    }


    function it_can_clear_the_index(Client $elasticsearch)
    {

        $elasticsearch->indices()->delete(['index' => $this->indexName]);

        $this->clearIndex();
    }



}

But it fails with error "Call to undefined method Prophecy\Prophecy\MethodProphecy::delete()".

How can the clearIndex-method be tested?

0 likes
3 replies
Adam Kelso's avatar

Have you tried explicitly stubbing out the delete method separate from the indices method?

function it_can_clear_the_index(Client $elasticsearch)
{
    $elasticsearch->indices();
    $elasticsearch->delete(['index' => $this->indexName]);

    // You may also need to add this line
    $this->beConstructedWith($elasticsearch);

    $this->clearIndex();
}

If indices returns something other than $this, then you would need to stub out whatever it returns.

JarekTkaczyk's avatar

@freekmurze First of all, there's a good practice of never calling anything further, than a method on your peer (Client as dependency in this case):

// wrong - what happens if indices() return value changes on the Client?
public function clearIndex()
{
   $this->elasticsearch->indices()->delete(['index' => $this->indexName]);
}

// right - you don't care about indices return value anymore, jut use Client's API
public function clearIndex()
{
   $this->elasticsearch->deleteIndices(['index' => $this->indexName]);
}

// example piece from the Client class
public function deleteIndices(array $indices)
{
  // the same might be the case here, unless indices() is some internal method
  $this->indices()->delete($indices);
}

Next, you are not testing anything there:

$elasticsearch->indices()->delete(['index' => $this->indexName]);
$this->clearIndex();

it's just calling the same methods. You likely want to test, that your method calls some other method available in the peer's (dependency) API:

protected $indexName = 'indexName'; // don't use __construct() for this, define it or use the setter

function it_can_clear_the_index(Client $elasticsearch)
{
  // expectation - I want it to happen
  $elasticsearch->deleteIndices(['index' => 'indexName'])->shouldBeCalled();

  // action - when I'm doing this
  $this->clearIndex();
}
1 like
silence's avatar

Came here looking for an answer and I ended up figuring out this by myself:

I am working in a Laravel component that interacts with the View layer, so I have this code in my class:

    return $this->view->make($custom, $data)->render();

View refers to View\Factory, it returns View\View and then View\View calls renders, it's like 2 dependencies, so I coded this:

    function it_renders_custom_templates(Factory $factory, View $view)
    {
        // Having
        $custom = 'custom.template';
        $data = array();
        $template = 'template';

        // Test that the dependency method is called and returns the other dependency
        $factory->make($custom, $data)->shouldBeCalled()->willReturn($view);

        // Test that the second dependency is called
        $view->render()->shouldBeCalled()->willReturn('<html>');

        $factory->exists("themes/$template")->shouldNotBeCalled();

        $this->render($custom, $data, $template)->shouldReturn('<html>');
    }

I don't know if it's the best way to solve but the test passes now

Please or to participate in this conversation.