diff --git a/composer.json b/composer.json index 66993d3..34c1c67 100644 --- a/composer.json +++ b/composer.json @@ -6,14 +6,24 @@ { "name": "Jan Wyszynski", "email": "jan.wyszynki@gmail.com" + }, + { + "name": "Willem Viljoen", + "email": "willemnviljoen@gmail.com" } ], "require": { - "elasticsearch/elasticsearch": "~1.0" + "elasticsearch/elasticsearch": "~1.0", + "codeception/codeception": "^2.1" + }, + "require-dev": { + "phpunit/phpunit": "~4.8", + "mockery/mockery": "~0.9" }, "autoload": { "psr-4": { - "Codeception\\Module\\": "src" + "Codeception\\Module\\": "src", + "Tests\\Codeception\\Module\\": "tests" } } } diff --git a/src/ElasticSearch.php b/src/ElasticSearch.php index 7049895..34fa9e8 100644 --- a/src/ElasticSearch.php +++ b/src/ElasticSearch.php @@ -5,38 +5,79 @@ namespace Codeception\Module; +use Codeception\Lib\ModuleContainer; use Codeception\Module; use Elasticsearch\Client; class ElasticSearch extends Module { - /** @var \Elasticsearch\Client */ - private $elasticSearch; + /** + * @var Client + */ + private $elasticSearch = null; - public function __construct($config = null) + public function __construct(ModuleContainer $moduleContainer, $config = null, Client $client = null) { - // terminology: see = isXyz => true/false, have = create, grab = get => data + $this->setModuleConfiguration($config); + $this->sanitizeModuleConfiguration(); + $this->setElasticSearchClient($client); + + parent::__construct($moduleContainer); + } - if (!isset($config['hosts'])) { + /** + * @param $config + */ + private function setModuleConfiguration($config) + { + $this->config = $config; + } + + private function sanitizeModuleConfiguration() + { + $this->guardThatConfigurationHasHosts(); + $this->wrapConfiguredHostsInArrayIfNeeded(); + } + + private function guardThatConfigurationHasHosts() + { + if (!isset($this->config['hosts'])) { throw new \Exception('please configure hosts for ElasticSearch codeception module'); } + } - if (isset($config['hosts']) && !is_array($config['hosts'])) { - $config['hosts'] = array($config['hosts']); + private function wrapConfiguredHostsInArrayIfNeeded() + { + if (!is_array($this->config['hosts'])) { + $this->config['hosts'] = array($this->config['hosts']); } - $this->config = (array)$config; + } - parent::__construct(); + /** + * @param Client $client + */ + private function setElasticSearchClient($client) + { + $this->elasticSearch = $client; } public function _initialize() { - /* - * elastic search config - * hosts - array of ES hosts - * dic - ES dictionary - */ + if ($this->doesNotHaveElasticSearchClient()) { + $this->buildElasticSearchClient(); + } + } + /** + * @return bool + */ + private function doesNotHaveElasticSearchClient() + { + return is_null($this->elasticSearch); + } + + private function buildElasticSearchClient() + { $this->elasticSearch = new Client($this->config); } @@ -49,7 +90,7 @@ public function _initialize() * * @return array */ - public function seeItemExistsInElasticsearch($index, $type, $id) + public function seeItemExistsInElasticSearch($index, $type, $id) { return $this->elasticSearch->exists( [ @@ -60,7 +101,6 @@ public function seeItemExistsInElasticsearch($index, $type, $id) ); } - /** * grab an item from search index * @@ -70,7 +110,7 @@ public function seeItemExistsInElasticsearch($index, $type, $id) * * @return array */ - public function grabAnItemFromElasticsearch($index = null, $type = null, $queryString = '*') + public function grabAnItemFromElasticSearch($index = null, $type = null, $queryString = '*') { $result = $this->elasticSearch->search( [ @@ -81,10 +121,68 @@ public function grabAnItemFromElasticsearch($index = null, $type = null, $queryS ] ); - return !empty($result['hits']['hits']) - ? $result['hits']['hits'][0]['_source'] - : array(); + if ($this->isEmptyResult($result)) { + return array(); + } + + return $this->getFirstItemFromResult($result); } + /** + * @param $result + * @return bool + */ + private function isEmptyResult($result) + { + return empty($result['hits']['hits']); + } + + /** + * @param $result + * @return mixed + */ + private function getFirstItemFromResult($result) + { + return $result['hits']['hits'][0]['_source']; + } + + public function getHosts() + { + return $this->config['hosts']; + } + + public function createIndexInElasticSearch($indexName) + { + $this->elasticSearch->indices()->create(['index' => $indexName]); + } + public function deleteIndexInElasticSearch($indexName) + { + $this->elasticSearch->indices()->delete(['index' => $indexName]); + } + + public function seeIndexExistsInElasticSearch($indexName) + { + $this->assertTrue($this->elasticSearch->indices()->exists(['index' => $indexName])); + } + + public function dontSeeIndexExistsInElasticSearch($indexName) + { + $this->assertFalse($this->elasticSearch->indices()->exists(['index' => $indexName])); + } + + public function indexAnItemInElasticSearch($indexName, $documentType, $documentBody, $id = null) + { + $params = [ + 'index' => $indexName, + 'type' => $documentType, + 'body' => $documentBody + ]; + + if (!is_null($id)) { + $params['id'] = $id; + } + + $this->elasticSearch->index($params); + } } \ No newline at end of file diff --git a/tests/ElasticSearchConfigurationTest.php b/tests/ElasticSearchConfigurationTest.php new file mode 100644 index 0000000..12f6dd9 --- /dev/null +++ b/tests/ElasticSearchConfigurationTest.php @@ -0,0 +1,47 @@ +container, null); + } + + /** + * @test + */ + public function shouldEncapsulateHostsInArrayIfNotEncapsulated() + { + $module = new ElasticSearch($this->container, ['hosts' => 'test.host.com']); + $hosts = $module->getHosts(); + $this->assertEquals('test.host.com', $hosts[0]); + } + + /** + * @test + * @expectedException \Exception + * @expectedExceptionMessage Could not resolve host: test.3.1415.nonexistent-host.com + * @fixme Find a way to test this without having to rely on an exception from across the boundary + */ + public function initializeShouldCreateClientWithConfiguredHostsIfNoClientIsPassedToConstructor() + { + $module = new ElasticSearch($this->container, ['hosts' => ['test.3.1415.nonexistent-host.com']]); + $module->_initialize(); + $module->seeItemExistsInElasticsearch('any-indexname', 'any-type', 'any-id'); + } +} \ No newline at end of file diff --git a/tests/ElasticSearchIndexManagementTest.php b/tests/ElasticSearchIndexManagementTest.php new file mode 100644 index 0000000..603795c --- /dev/null +++ b/tests/ElasticSearchIndexManagementTest.php @@ -0,0 +1,94 @@ +module->createIndexInElasticsearch('index-name'); + $this->indicesNamespace->shouldHaveReceived('create')->with(m::subset(['index' => 'index-name'])); + } + + /** + * @test + */ + public function deleteIndexInElasticsearchShouldCallDeleteOnClientIndicesWithIndexName() + { + $this->module->deleteIndexInElasticsearch('index-name'); + $this->indicesNamespace->shouldHaveReceived('delete')->with(m::subset(['index' => 'index-name'])); + } + + /** + * @test + */ + public function seeIndexExistsInElasticsearchShouldCallExistsOnClientIndicesWithIndexName() + { + $this->module->seeIndexExistsInElasticsearch('index-name'); + $this->indicesNamespace->shouldHaveReceived('exists')->with(m::subset(['index' => 'index-name'])); + } + + /** + * @test + */ + public function seeIndexExistsInElasticsearchShouldDoNothingIfIndexExists() + { + $this->indicesNamespace->shouldReceive('exists')->andReturn(true); + $this->module->seeIndexExistsInElasticsearch('index-name'); + } + + /** + * @test + * @expectedException \PHPUnit_Framework_ExpectationFailedException + */ + public function seeIndexExistsInElasticsearchShouldHaveFailingAssertionIfIndexDoesNot() + { + $this->indicesNamespace->shouldReceive('exists')->andReturn(false); + $this->module->seeIndexExistsInElasticsearch('index-name'); + } + + /** + * @test + */ + public function dontSeeIndexExistsInElasticsearchShouldDoNothingIfIndexDoesNotExist() + { + $this->indicesNamespace->shouldReceive('exists')->andReturn(false); + $this->module->dontSeeIndexExistsInElasticsearch('index-name'); + } + + /** + * @test + * @expectedException \PHPUnit_Framework_ExpectationFailedException + */ + public function dontSeeIndexExistsInElasticsearchShouldHaveFailingAssertionIfIndexExists() + { + $this->indicesNamespace->shouldReceive('exists')->andReturn(true); + $this->module->dontSeeIndexExistsInElasticsearch('index-name'); + } + + public function setUp() + { + parent::setUp(); + + $this->indicesNamespace = m::mock('\Elasticsearch\Namespaces\IndicesNamespace'); + $this->indicesNamespace->shouldReceive('exists')->andReturn(true)->byDefault(); + $this->indicesNamespace->shouldIgnoreMissing(); + $this->client->shouldReceive('indices')->andReturn($this->indicesNamespace)->byDefault(); + } +} diff --git a/tests/ElasticSearchIndexingTest.php b/tests/ElasticSearchIndexingTest.php new file mode 100644 index 0000000..fe8e03d --- /dev/null +++ b/tests/ElasticSearchIndexingTest.php @@ -0,0 +1,67 @@ +module->indexAnItemInElasticSearch('index-name', null, null); + $this->client->shouldHaveReceived('index')->with(m::subset(['index' => 'index-name'])); + } + + /** + * @test + */ + public function indexAnItemInElasticSearchShouldCallIndexOnClientWithType() + { + $this->module->indexAnItemInElasticSearch(null, 'document-type', null); + $this->client->shouldHaveReceived('index')->with(m::subset(['type' => 'document-type'])); + } + + /** + * @test + */ + public function indexAnItemInElasticSearchShouldCallIndexOnClientWithIdIfSpecified() + { + $this->module->indexAnItemInElasticSearch(null, null, null, 123); + $this->client->shouldHaveReceived('index')->with(m::subset(['id' => 123])); + } + + /** + * @test + */ + public function indexAnItemInElasticSearchShouldCallIndexOnClientWithoutIdIfNotSpecified() + { + $this->module->indexAnItemInElasticSearch(null, null, null); + $this->client->shouldHaveReceived('index')->with(m::on(function ($actual) { + return !array_key_exists('id', $actual); + })); + } + + /** + * @test + */ + public function indexAnItemInElasticSearchShouldCallIndexOnClientWithDocumentBody() + { + $documentBody = [ + 'apples' => 1, + 'oranges' => 2 + ]; + + $this->module->indexAnItemInElasticSearch(null, null, $documentBody); + $this->client->shouldHaveReceived('index')->with(m::subset(['body' => $documentBody])); + } +} diff --git a/tests/ElasticSearchTestCase.php b/tests/ElasticSearchTestCase.php new file mode 100644 index 0000000..e4b64c1 --- /dev/null +++ b/tests/ElasticSearchTestCase.php @@ -0,0 +1,44 @@ +container = m::mock('\Codeception\Lib\ModuleContainer'); + $this->client = m::mock('\Elasticsearch\Client'); + $this->client->shouldIgnoreMissing(); + $this->module = new ElasticSearch($this->container, ['hosts' => []], $this->client); + } + + public function tearDown() + { + m::close(); + } +} \ No newline at end of file diff --git a/tests/ElasticSearch_grabAnItemFromElasticsearchTest.php b/tests/ElasticSearch_grabAnItemFromElasticsearchTest.php new file mode 100644 index 0000000..acf020f --- /dev/null +++ b/tests/ElasticSearch_grabAnItemFromElasticsearchTest.php @@ -0,0 +1,72 @@ +module->grabAnItemFromElasticsearch('index-name'); + $this->client->shouldHaveReceived('search')->with(m::subset(['index' => 'index-name'])); + } + + /** + * @test + */ + public function grabAnItemFromElasticsearchShouldCallSearchWithTypeOnClient() + { + $this->module->grabAnItemFromElasticsearch(null, 'some-document-type'); + $this->client->shouldHaveReceived('search')->with(m::subset(['type' => 'some-document-type'])); + } + + /** + * @test + */ + public function grabAnItemFromElasticsearchShouldCallSearchWithQueryStringOnClient() + { + $this->module->grabAnItemFromElasticsearch(null, null, 'something'); + $this->client->shouldHaveReceived('search')->with(m::subset(['q' => 'something'])); + } + + /** + * @test + */ + public function grabAnItemFromElasticsearchShouldCallSearchWithSizeOneOnClient() + { + $this->module->grabAnItemFromElasticsearch(null, null, null); + $this->client->shouldHaveReceived('search')->with(m::subset(['size' => 1])); + } + + /** + * @test + */ + public function grabAnItemFromElasticsearchShouldReturnEmptyArrayIfThereAreNoHits() + { + $this->client->shouldReceive('search')->andReturn(['hits' => ['hits' => []]]); + $item = $this->module->grabAnItemFromElasticsearch(null, null, null); + $this->assertEmpty($item); + } + + /** + * @test + */ + public function grabAnItemFromElasticsearchShouldReturnFirstSourceArrayIfThereIsAHits() + { + $expectedItem = ['apples' => 1, 'oranges' => 2]; + $this->client->shouldReceive('search')->andReturn(['hits' => ['hits' => [['_source' => $expectedItem]]]]); + $actualItem = $this->module->grabAnItemFromElasticsearch(null, null, null); + $this->assertEquals($expectedItem, $actualItem); + } +} diff --git a/tests/ElasticSearch_seeItemExistsInElasticsearchTest.php b/tests/ElasticSearch_seeItemExistsInElasticsearchTest.php new file mode 100644 index 0000000..c32e6bc --- /dev/null +++ b/tests/ElasticSearch_seeItemExistsInElasticsearchTest.php @@ -0,0 +1,42 @@ +module->seeItemExistsInElasticsearch('index-name', null, null); + $this->client->shouldHaveReceived('exists')->with(m::subset(['index' => 'index-name']))->once(); + } + + /** + * @test + */ + public function seeItemExistsInElasticsearchShouldCallExistsWithTypeOnClient() + { + $this->module->seeItemExistsInElasticsearch(null, 'document-type', null); + $this->client->shouldHaveReceived('exists')->with(m::subset(['type' => 'document-type']))->once(); + } + + /** + * @test + */ + public function seeItemExistsInElasticsearchShouldCallExistsWithIdOnClient() + { + $this->module->seeItemExistsInElasticsearch(null, null, 'document-id'); + $this->client->shouldHaveReceived('exists')->with(m::subset(['id' => 'document-id']))->once(); + } +} \ No newline at end of file