3

I have an application with an existing set of unit tests which are using SQLite as the DB. I have recently added search capabilities via ES which have replaced many of the endpoint actions that used to query the DB directly. I want to test all of the business logic involved with these endpoints without testing ES itself, which means no ES server available. I plan to test ES itself in a set of integration tests to be run less frequently.

My problem is trying to track down exactly what is going on with the execution flow.

My first inclination was to simply create a mock object of the ES Finder that FOSElasticaBundle creates for my index. Because I'm using pagination, it turned out to be more complex than I thought:

    // code context: test method in unit test extending Symfony's WebTestCase
    $client = $this->getClient();


    $expectedHitCount = 10;

    // Setup real objects which (as far as I can tell) don't act upon the ES client
    // and instead only hold / manipulate the data.
    $responseString = file_get_contents(static::SEARCH_RESULT_FILE_RESOURCE);
    $query = SearchRepository::getProximitySearchQuery($lat, $lng, $radius, $offset, $limit);
    $response = new Response($responseString, 200);        
    $resultSet = new RawPartialResults(new ResultSet($response, $query ));

    // Create a mock pagination adapter which is what my service expects to be returned from
    // the search repository.
    $adapter = $this->getMockBuilder('FOS\ElasticaBundle\Paginator\RawPaginatorAdapter')
                    ->disableOriginalConstructor()
                    ->getMock();
    $adapter->method('getTotalHits')->will($this->returnValue($expectedTotalCount));
    $adapter->method('getResults')->will($this->returnValue($resultSet));
    $adapter->method('getQuery')->will($this->returnValue($query));

    $es = $this->getMockBuilder(get_class($client->getContainer()->get(static::ES_FINDER_SERVICE)))
               ->disableOriginalConstructor()
               ->getMock();
    $es->method('createPaginatorAdapter')->will($this->returnValue($adapter));

    // Replace the client container's service definition with our mock object
    $client->getContainer()->set(static::ES_FINDER_SERVICE, $es);

This actually works all the way until I return the view from my controller. My service gets back the mock paginatior adapter with the pre-popuated result set from the JSON search response I have stored in a file (and subsequently passed into my ResultSet object). However, once I return the view, there seems to be a listener involved that tries to query ES again with the Query instead of using the ResultSet I already passed in.

I can't seem to find this listener. I also don't understand why it would try to query when a ResuletSet already exists.

I am using FOSRestBundle, as well, and making use of their ViewListener to auto-serialize whatever I return. I don't see any suspects in that flow, either. I think it may have something to do with the serialization of the result set, but so far haven't been able to track the offending code down.

Has anyone tried to do something similar to this before and have any suggestions on either how to debug my current setup or an alternative, better setup for mocking ES for this type of test?

Jason McClellan
  • 2,931
  • 3
  • 23
  • 32

2 Answers2

3

After digging around I found an alternative solution that does not involve using mock objects. I am going to leave this open for the time being in case someone has a better approach, but the approach I decided to take in the mean time is to override the Client in my test environment.

FOSElasticaBundle has an example for overriding the client here: https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/master/Resources/doc/cookbook/suppress-server-errors.md

I was able to override the client in such a way that I could create a unique key from the request and then provide responses based on that key, essentially stubbing the server for all known requests. For requests that don't match I return a default empty response. This works well enough for me.

Client Code

<?php

namespace Acme\DemoBundle\Tests\Elastica;

use Elastica\Request;
use Elastica\Response;
use FOS\ElasticaBundle\Client as BaseClient;

class Client extends BaseClient
{
    /**
     * This array translates a key which is the md5 hash of the Request::toString() into
     * a human friendly name so that we can load the proper response from a file in the
     * file system.
     * 
     * @var array
     */
    protected $responseLookup = array(
        '7fea3dda860a424aa974b44f508b6678' => 'proximity-search-response.json'
    );

    /**
     * {@inheritdoc}
     */
    public function request($path, $method = Request::GET, $data = array(), array $query = array())
    {
        $request = new Request($path, $method, $data, $query);
        $requestKey = md5($request->toString());
        $this->_log($request);
        $this->_log("Test request lookup key: $requestKey");

        if (!isset($this->responseLookup[$requestKey])
            || !$response = file_get_contents(__DIR__ . "/../DataFixtures/Resources/search/{$this->responseLookup[$requestKey]}")) {
            return $this->getNullResponse();
        }

        return new Response($response);
    }

    public function getNullResponse()
    {
        $this->_log("Returning NULL response");
        return new Response('{"took":0,"timed_out":false,"hits":{"total":0,"max_score":0,"hits":[]}}');
    }
}

Configuration Change

// file: config_test.yml
parameters:
    fos_elastica.client.class: Acme\DemoBundle\Tests\Elastica\Client

Sample Response File (proximity-search-response.json)

{
  "took": 7,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": null,
    "hits": [
      {
        "_index": "search",
        "_type": "place",
        "_id": "1",
        "_score": null,
        "_source": {
          "location": "40.849100,-73.644800",
          "id": 1,
          "name": "My Place"
        },
        "sort": [
          322.52855474383045
        ]
      }
    ]
  }
}

This solution works well and is fast, but the maintenance is a pain. If anything about the request changes, you need to retrieve the new request key from the log, update it in the array, and update the file with the new response data for the new request. I generally just curl the server directly and modify it from there.

I would love to see any other solutions that may be simpler, but I hope this helps someone else in the meantime!

Jason McClellan
  • 2,931
  • 3
  • 23
  • 32
  • that does not work for me .. I m using fos elastica finder class , may be it s related ? – Charles-Antoine Fournel Feb 10 '16 at 13:59
  • I'm not sure. This is an old project and I haven't had any updates to it in a long while. However, the solution that @seltzlab mentions below - disabling the listeners, in conjunction with mocking the pagination adapter as I have done in my initial question code snippet may work for you. – Jason McClellan Feb 16 '16 at 10:52
1

you can try to disable the event listeners in your config_test.yml (or whatever is your test environment name).

fos_elastica:
    indexes:
        your_index_name:
            types:
                your_type_name:
                    persistence:
                        listener:
                            insert: false
                            update: false
                            delete: false
seltzlab
  • 332
  • 8
  • 13