Skip to content
Snippets Groups Projects
Commit 626b2a9b authored by Alexey Korepov's avatar Alexey Korepov
Browse files

Issue #3313778 by Murz: 3313778-database-stub

parent 52131406
No related branches found
Tags 1.0.0-alpha5
No related merge requests found
<?php
namespace Drupal\test_helpers;
use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\Tests\Core\Database\Stub\StubPDO;
/**
* The ConnectionStubFactory class.
*
* A stub for class Drupal\Driver\Database\fake\Connection.
*/
class ConnectionStub extends Connection {
/**
* The UnitsTestHelpers.
*
* @var Drupal\test_helpers\UnitTestHelpers
*/
protected $unitTestHelpers;
/**
* The static storage for execute functions.
*
* @var array
*/
protected $stubExecuteHandlers;
/**
* Constructs a new object.
*/
public function __construct() {
$this->unitTestHelpers = UnitTestHelpers::getInstance();
$this->pdoMock = $this->unitTestHelpers->createMock(StubPDO::class);
$this->connectionOptions = [
'namespace' => 'Drupal\sqlite\Driver\Database\sqlite',
];
parent::__construct($this->pdoMock, $this->connectionOptions);
}
private function mockExecuteForMethod($method, $arguments, $executeFunction) {
$originalMethod = parent::$method(...$arguments);
$class = \get_class($originalMethod);
$mockedMethod = $this->unitTestHelpers->createPartialMockWithCostructor($class, [
'execute',
], [$this, ...$arguments]);
$executeFunction = $this->stubExecuteHandlers[$method]
?? $this->stubExecuteHandlers['all']
?? function () {
return 'default';
};
UnitTestHelpers::bindClosureToClassMethod($executeFunction, $mockedMethod, 'execute');
return $mockedMethod;
}
public function select($table, $alias = NULL, array $options = []) {
$arguments = \func_get_args();
$executeFunction = function () {
return 123;
};
$select = $this->mockExecuteForMethod('select', $arguments, $executeFunction);
return $select;
}
public function stubAddExecuteHandler(\Closure $executeFunction, string $method = 'all') {
$this->stubExecuteHandlers[$method] = $executeFunction;
}
}
<?php
namespace Drupal\test_helpers;
use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\Tests\Core\Database\Stub\StubPDO;
/**
* The ConnectionStubFactory class.
*
* A stub for class Drupal\Driver\Database\fake\Connection.
*/
class DatabaseStubFactory {
/**
* The UnitsTestHelpers.
*
* @var Drupal\test_helpers\UnitTestHelpers
*/
protected $unitTestHelpers;
/**
* Constructs a new object.
*/
public function __construct() {
$this->unitTestHelpers = UnitTestHelpers::getInstance();
$this->pdoMock = $this->unitTestHelpers->createMock(StubPDO::class);
$this->connectionSettings = [];
}
public function get() {
/**
* @todo Replace Drupal\sqlite\Driver\Database\sqlite\Connection to
* Drupal\Driver\Database\fake\Connection.
* require_once DRUPAL_ROOT . '/core/tests/fixtures/database_drivers/core/corefake/Connection.php';
* does not work because of namespaces, check the answers
* https://stackoverflow.com/questions/57322376/cannot-load-class-with-require-once-when-using-namespace
* for possible solutions.
*/
$connection = $this->unitTestHelpers->createPartialMockWithCostructor(
Connection::class,
['select'],
[$this->pdoMock, $this->connectionSettings]
);
return $connection;
}
/**
* Registers the class as the 'database' service.
*/
public function registerService() {
$connection = new ConnectionStub();
UnitTestHelpers::addToContainer('database', $connection);
return $connection;
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\test_helpers;
use Drupal\Core\Database\Query\ConditionInterface;
use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\Entity\Query\QueryFactoryInterface;
......@@ -25,15 +26,12 @@ class EntityQueryServiceStub implements QueryFactoryInterface {
* Gets the query for entity type.
*/
public function get($entityType, $conjunction) {
$entityTypeId = $entityType->id();
if (isset($this->executeFunctions[$entityTypeId][$conjunction])) {
$executeFunction = $this->executeFunctions[$entityTypeId][$conjunction];
}
else {
$executeFunction = function () {
$executeFunction =
$this->executeFunctions[$entityType->id()]
?? $this->executeFunctions['all']
?? function () {
return [];
};
}
$query = $this->queryStubFactory->get($entityType, $conjunction, $executeFunction);
return $query;
}
......@@ -42,25 +40,19 @@ class EntityQueryServiceStub implements QueryFactoryInterface {
* Gets the aggregate query for entity type.
*/
public function getAggregate($entityType, $conjunction) {
$entityTypeId = $entityType->id();
if (isset($this->executeFunctions[$entityTypeId][$conjunction])) {
$executeFunction = $this->executeFunctions[$entityTypeId][$conjunction];
}
else {
$executeFunction = function () {
return [];
};
}
// @todo Implement a getAggregate call.
$query = $this->queryStubFactory->get($entityType, $conjunction, $executeFunction);
return $query;
// @todo Implement a custom getAggregate call.
return $this->get($entityType, $conjunction);
}
/**
* Adds an execute callback function to the particular entity type.
*/
public function stubAddExecuteFunction(string $entityTypeId, string $conjunction, callable $function) {
$this->executeFunctions[$entityTypeId][$conjunction] = $function;
public function stubAddExecuteHandler(callable $function, string $entityTypeId = 'all') {
$this->executeFunctions[$entityTypeId] = $function;
}
public function stubCheckConditionsMatch(ConditionInterface $conditionsExpected, $onlyListed = FALSE) {
return UnitTestHelpers::matchConditions($conditionsExpected, $this->condition, $onlyListed);
}
}
......@@ -60,61 +60,10 @@ class EntityQueryStubFactory {
UnitTestHelpers::bindClosureToClassMethod($executeFunction, $queryStub, 'execute');
UnitTestHelpers::bindClosureToClassMethod(function (Condition $conditionsExpected, $onlyListed = FALSE) {
return EntityQueryStubFactory::matchConditions($conditionsExpected, $this->condition, $onlyListed);
return UnitTestHelpers::matchConditions($conditionsExpected, $this->condition, $onlyListed);
}, $queryStub, 'stubCheckConditionsMatch');
return $queryStub;
}
/**
* Performs matching of passed conditions with the query.
*/
public static function matchConditions(Condition $conditionsExpectedObject, Condition $conditionsObject, $onlyListed = FALSE): bool {
if (strcasecmp($conditionsObject->getConjunction(), $conditionsExpectedObject->getConjunction()) != 0) {
return FALSE;
}
$conditions = $conditionsObject->conditions();
$conditionsExpected = $conditionsExpectedObject->conditions();
$conditionsFound = [];
foreach ($conditions as $condition) {
foreach ($conditionsExpected as $delta => $conditionExpected) {
if (EntityQueryStubFactory::matchCondition($conditionExpected, $condition, $onlyListed)) {
$conditionsFound[$delta] = TRUE;
}
}
}
if (count($conditionsFound) != count($conditionsExpected)) {
return FALSE;
}
if ($onlyListed && (count($conditions) != count($conditionsExpected))) {
return FALSE;
}
return TRUE;
}
/**
* Performs matching of a single condition with expected.
*/
public static function matchCondition(array $conditionExpected, array $conditionExists, $onlyListed = FALSE): bool {
if (is_object($conditionExists['field'] ?? NULL)) {
if (!is_object($conditionExpected['field'] ?? NULL)) {
return FALSE;
}
return self::matchConditions($conditionExpected['field'], $conditionExists['field'], $onlyListed);
}
if (($conditionExpected['field'] ?? NULL) != ($conditionExists['field'] ?? NULL)) {
return FALSE;
}
if (($conditionExpected['value'] ?? NULL) != ($conditionExists['value'] ?? NULL)) {
return FALSE;
}
if (($conditionExpected['operator'] ?? NULL) != ($conditionExists['operator'] ?? NULL)) {
return FALSE;
}
if (($conditionExpected['langcode'] ?? NULL) != ($conditionExists['langcode'] ?? NULL)) {
return FALSE;
}
return TRUE;
}
}
<?php
namespace Drupal\test_helpers;
use Drupal\Core\Entity\EntityTypeManager;
/**
* The EntityTypeManagerStub class for internal usage only.
*
* This is an utility class for creating a partial mock with required interface.
*/
class EntityTypeManagerStub extends EntityTypeManager implements EntityTypeManagerStubInterface {
public function stubAddDefinition(string $pluginId, object $definition = NULL, $forceOverride = FALSE) {
}
public function stubGetOrCreateHandler(string $handlerType, string $entityTypeId, object $handler = NULL, $forceOverride = FALSE) {
}
public function stubGetOrCreateStorage(string $entityClass, object $storage = NULL, $forceOverride = FALSE) {
}
public function stubInit() {
}
public function stubReset() {
}
}
......@@ -4,7 +4,6 @@ namespace Drupal\test_helpers;
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManager;
/**
* The EntityTypeManagerStubFactory class.
......@@ -14,6 +13,11 @@ class EntityTypeManagerStubFactory {
/**
* Constructs a new FieldTypeManagerStub.
*/
/**
* @var \Drupal\test_helpers\UnitTestHelpers
*/
private $unitTestHelpers;
public function __construct() {
$this->unitTestHelpers = UnitTestHelpers::getInstance();
UnitTestHelpers::addToContainer('entity.repository', $this->unitTestHelpers->createMock(EntityRepositoryInterface::class));
......@@ -39,9 +43,9 @@ class EntityTypeManagerStubFactory {
/**
* Constructs a new FieldTypeManagerStub.
*/
public function create() {
/** @var \Drupal\Core\Entity\EntityTypeManager|\PHPUnit\Framework\MockObject\MockObject $entityTypeManagerStub */
$entityTypeManagerStub = $this->unitTestHelpers->createPartialMock(EntityTypeManager::class, [
public function create(): EntityTypeManagerStubInterface {
/** @var \Drupal\test_helpers\EntityTypeManagerStubInterface|\PHPUnit\Framework\MockObject\MockObject $entityTypeManagerStub */
$entityTypeManagerStub = $this->unitTestHelpers->createPartialMock(EntityTypeManagerStub::class, [
'findDefinitions',
// Custom helper functions for the stub:
......
<?php
namespace Drupal\test_helpers;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* The EntityTypeManagerStubFactory class.
*/
interface EntityTypeManagerStubInterface extends EntityTypeManagerInterface {
public function stubAddDefinition(string $pluginId, object $definition = NULL, $forceOverride = FALSE);
public function stubGetOrCreateHandler(string $handlerType, string $entityTypeId, object $handler = NULL, $forceOverride = FALSE);
public function stubGetOrCreateStorage(string $entityClass, object $storage = NULL, $forceOverride = FALSE);
public function stubInit();
public function stubReset();
}
<?php
namespace Drupal\test_helpers;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\Sql\Query;
/**
* The Entity Storage Stub class.
*
* A stub for class Drupal\Core\Entity\Query\Sql\QueryFactory.
*/
class QueryStubFactory implements QueryStubFactoryInterface {
/**
* Constructs a QueryStubFactory object.
*/
public function __construct() {
}
/**
* Instantiates an entity query for a given entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
* The entity type definition.
* @param string $conjunction
* The operator to use to combine conditions: 'AND' or 'OR'.
* @param \Closure $executeFunction
* The function to use for `execute` call.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* An entity query for a specific configuration entity type.
*/
public function get(EntityTypeInterface $entityType = NULL, string $conjunction = 'AND', \Closure $executeFunction = NULL) {
if ($executeFunction === NULL) {
$executeFunction = function () {
return [];
};
}
if ($entityType === NULL) {
$entityType = $this->unitTestHelpers->createMock(EntityTypeInterface::class);
}
$queryStub = $this->unitTestHelpers->createPartialMockWithCostructor(Query::class, [
'execute',
], [$entityType, $conjunction, $this->dbConnection, $this->namespaces], [
'stubCheckConditionsMatch',
]);
UnitTestHelpers::bindClosureToClassMethod($executeFunction, $queryStub, 'execute');
UnitTestHelpers::bindClosureToClassMethod(function (Condition $conditionsExpected, $onlyListed = FALSE): bool {
return UnitTestHelpers::matchConditions($conditionsExpected, $this->condition, $onlyListed);
}, $queryStub, 'stubCheckConditionsMatch');
return $queryStub;
}
/**
* Performs matching of passed conditions with the query.
*/
public static function matchConditions(Condition $conditionsExpectedObject, Condition $conditionsObject, $onlyListed = FALSE): bool {
if (strcasecmp($conditionsObject->getConjunction(), $conditionsExpectedObject->getConjunction()) != 0) {
return FALSE;
}
$conditions = $conditionsObject->conditions();
$conditionsExpected = $conditionsExpectedObject->conditions();
$conditionsFound = [];
foreach ($conditions as $condition) {
foreach ($conditionsExpected as $delta => $conditionExpected) {
if (EntityQueryStubFactory::matchCondition($conditionExpected, $condition, $onlyListed)) {
$conditionsFound[$delta] = TRUE;
}
}
}
if (count($conditionsFound) != count($conditionsExpected)) {
return FALSE;
}
if ($onlyListed && (count($conditions) != count($conditionsExpected))) {
return FALSE;
}
return TRUE;
}
/**
* Performs matching of a single condition with expected.
*/
public static function matchCondition(array $conditionExpected, array $conditionExists, $onlyListed = FALSE): bool {
if (is_object($conditionExists['field'] ?? NULL)) {
if (!is_object($conditionExpected['field'] ?? NULL)) {
return FALSE;
}
return self::matchConditions($conditionExpected['field'], $conditionExists['field'], $onlyListed);
}
if (($conditionExpected['field'] ?? NULL) != ($conditionExists['field'] ?? NULL)) {
return FALSE;
}
if (($conditionExpected['value'] ?? NULL) != ($conditionExists['value'] ?? NULL)) {
return FALSE;
}
if (($conditionExpected['operator'] ?? NULL) != ($conditionExists['operator'] ?? NULL)) {
return FALSE;
}
if (($conditionExpected['langcode'] ?? NULL) != ($conditionExists['langcode'] ?? NULL)) {
return FALSE;
}
return TRUE;
}
}
<?php
namespace Drupal\test_helpers;
use Drupal\Core\Database\Query\Condition;
/**
* The Entity Storage Stub class.
*
* A stub for class Drupal\Core\Entity\Query\Sql\QueryFactory.
*/
interface QueryStubItemInterface {
/**
* Instantiates an entity query for a given entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
* The entity type definition.
* @param string $conjunction
* The operator to use to combine conditions: 'AND' or 'OR'.
* @param \Closure $executeFunction
* The function to use for `execute` call.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* An entity query for a specific configuration entity type.
*/
public function stubCheckConditionsMatch(Condition $conditionsExpected, $onlyListed = FALSE): bool;
}
......@@ -20,20 +20,23 @@ trait SingletonTrait {
}
/**
* Is not allowed to call from outside to prevent from creating multiple instances,
* to use the singleton, you have to obtain the instance from Singleton::getInstance() instead
* Is not allowed to call from outside to prevent from creating multiple
* instances, to use the singleton, you have to obtain the instance from
* Singleton::getInstance() instead.
*/
private function __construct() {
}
/**
* Prevents the instance from being cloned (which would create a second instance of it)
* Prevents the instance from being cloned (which would create a second
* instance of it).
*/
private function __clone() {
}
/**
* Prevents from being unserialized (which would create a second instance of it)
* Prevents from being unserialized (which would create a second instance
* of it).
*/
public function __wakeup() {
throw new \Exception("Cannot unserialize singleton");
......
......@@ -4,7 +4,9 @@ namespace Drupal\test_helpers;
use Drupal\Component\Annotation\Doctrine\SimpleAnnotationReader;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Database\Query\ConditionInterface as QueryConditionInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\Query\ConditionInterface as EntityQueryConditionInterface;
use Drupal\test_helpers\Traits\SingletonTrait;
use Drupal\Tests\UnitTestCase;
use PHPUnit\Framework\MockObject\MockObject;
......@@ -15,21 +17,40 @@ use Symfony\Component\Yaml\Yaml;
*/
class UnitTestHelpers extends UnitTestCase {
use SingletonTrait {
// This trick is to allow use of the class not as a singleton too.
__construct as __originalConstruct;
}
/**
* Gets an accessible method from class using reflection.
* A dummy constructor.
*/
public static function getAccessibleMethod(object $className, string $methodName): \ReflectionMethod {
$class = new \ReflectionClass($className);
$method = $class
public function __construct() {
}
/**
* Gets protected method from a class using reflection.
*/
public static function getProtectedMethod(object $class, string $methodName): \ReflectionMethod {
$reflection = new \ReflectionClass($class);
$method = $reflection
->getMethod($methodName);
$method
->setAccessible(TRUE);
return $method;
}
/**
* Gets a protected property from a class using reflection.
*/
public static function getProtectedProperty(object $class, string $propertyName) {
$reflection = new \ReflectionClass($class);
$property = $reflection
->getProperty($propertyName);
$property
->setAccessible(TRUE);
return $property->getValue($class);
}
/**
* Parses the annotation for a class and gets the definition.
*/
......@@ -177,6 +198,74 @@ class UnitTestHelpers extends UnitTestCase {
return parent::createPartialMock($originalClassName, $methods);
}
/**
* Performs matching of passed conditions with the query.
*/
public static function matchConditions(object $conditionsExpectedObject, object $conditionsObject, $onlyListed = FALSE): bool {
if ($conditionsObject instanceof EntityQueryConditionInterface) {
if (strcasecmp($conditionsObject->getConjunction(), $conditionsExpectedObject->getConjunction()) != 0) {
return FALSE;
}
}
elseif ($conditionsObject instanceof QueryConditionInterface) {
if (strcasecmp($conditionsObject->conditions()['#conjunction'], $conditionsExpectedObject->conditions()['#conjunction']) != 0) {
return FALSE;
}
}
else {
throw new \Exception("Conditions should implement Drupal\Core\Entity\Query\ConditionInterface or Drupal\Core\Database\Query\ConditionInterface.");
}
$conditions = $conditionsObject->conditions();
unset($conditions['#conjunction']);
$conditionsExpected = $conditionsExpectedObject->conditions();
unset($conditionsExpected['#conjunction']);
$conditionsFound = [];
foreach ($conditions as $condition) {
foreach ($conditionsExpected as $conditionsExpectedDelta => $conditionExpected) {
if (is_object($condition['field']) || is_object($conditionExpected['field'])) {
if (!is_object($condition['field']) || !is_object($conditionExpected['field'])) {
continue;
}
return self::matchConditions($conditionExpected['field'], $condition['field'], $onlyListed);
}
if (self::isNestedArraySubsetOf($condition, $conditionExpected)) {
$conditionsFound[$conditionsExpectedDelta] = TRUE;
}
}
}
if (count($conditionsFound) != count($conditionsExpected)) {
return FALSE;
}
if ($onlyListed && (count($conditions) != count($conditionsExpected))) {
return FALSE;
}
return TRUE;
}
/**
* Performs check if the actial array is a subset of expected.
*/
public static function isNestedArraySubsetOf($array, $subset): bool {
if (!is_array($array) || !is_array($subset)) {
return FALSE;
}
$result = array_uintersect_assoc($subset, $array, self::class . '::isValueSubsetOfCallback');
return $result == $subset;
}
/**
* Internal callback helper function for array_uintersect.
*
* Should be public to be available as a callback.
*/
private static function isValueSubsetOfCallback($value, $expected): int {
// The callback function for array_uintersect should return
// integer instead of bool (-1, 0, 1).
if (is_array($expected)) {
return self::isNestedArraySubsetOf($value, $expected) ? 0 : -1;
}
return ($value == $expected) ? 0 : -1;
}
/**
* {@inheritdoc}
......
......@@ -30,13 +30,20 @@ class EntityQueryServiceStubTest extends UnitTestCase {
* @covers ::get
*/
public function testEntityQueryService() {
\Drupal::service('entity_type.manager')->stubGetOrCreateStorage(Node::class);
/** @var \Drupal\test_helpers\EntityTypeManagerStubInterface $entityTypeManager */
$entityTypeManager = \Drupal::service('entity_type.manager');
$entityTypeManager->stubGetOrCreateStorage(Node::class);
// Creating a custom function to generate the query result.
$titleValues = ['Title 1', 'Title 2'];
$entityQueryTestResult = ['1', '42'];
/** @var \Drupal\Tests\test_helpers\Unit\EntityQueryServiceStubTest $testClass */
$testClass = $this;
\Drupal::service('entity.query.sql')->stubAddExecuteFunction('node', 'AND', function () use ($entityQueryTestResult, $titleValues, $testClass) {
/** @var Drupal\test_helpers\EntityQueryServiceStub $entityQuerySql*/
$entityQuerySql = \Drupal::service('entity.query.sql');
$entityQuerySql->stubAddExecuteHandler(function () use ($entityQueryTestResult, $titleValues, $testClass) {
/** @var \Drupal\Core\Database\Query\SelectInterface|\Drupal\test_helpers\QueryStubItemInterface $this */
// Checking that mandatory conditions are present in the query.
$conditionsMandatory = $this->andConditionGroup();
$conditionsMandatory->condition('title', $titleValues, 'IN');
......@@ -50,6 +57,10 @@ class EntityQueryServiceStubTest extends UnitTestCase {
// Checking onlyListed mode returns false, because we have more conditions.
$testClass->assertFalse($this->stubCheckConditionsMatch($conditionsMandatory, TRUE));
// Checking onlyListed mode returns true with exact conditions list.
$orConditionGroup->condition('field_size', 'XL');
$testClass->assertTrue($this->stubCheckConditionsMatch($conditionsMandatory, TRUE));
// Checking that wrong conditions check is return FALSE.
$conditionsMandatoryWrong1 = $this->orConditionGroup();
$conditionsMandatoryWrong1->condition('title', $titleValues, 'IN');
......@@ -81,7 +92,7 @@ class EntityQueryServiceStubTest extends UnitTestCase {
// Returning a pre-defined result for the query.
return $entityQueryTestResult;
});
}, 'node');
$entityQuery = \Drupal::service('entity_type.manager')
->getStorage('node')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment