Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • project/test_helpers
  • issue/test_helpers-3310084
  • issue/test_helpers-3310121
  • issue/test_helpers-3310321
  • issue/test_helpers-3311368
  • issue/test_helpers-3312304
  • issue/test_helpers-3313778
  • issue/test_helpers-3313859
  • issue/test_helpers-3314742
  • issue/test_helpers-3314743
  • issue/test_helpers-3314746
  • issue/test_helpers-3314859
  • issue/test_helpers-3315119
  • issue/test_helpers-3314846
  • issue/test_helpers-3315975
  • issue/test_helpers-3316195
  • issue/test_helpers-3316504
  • issue/test_helpers-3316845
  • issue/test_helpers-3326156
  • issue/test_helpers-3326541
  • issue/test_helpers-3326037
  • issue/test_helpers-3328845
  • issue/test_helpers-3326159
  • issue/test_helpers-3332840
  • issue/test_helpers-3333336
  • issue/test_helpers-3333376
  • issue/test_helpers-3333515
  • issue/test_helpers-3334101
  • issue/test_helpers-3343623
  • issue/test_helpers-3336364
  • issue/test_helpers-3336394
  • issue/test_helpers-3336574
  • issue/test_helpers-3336575
  • issue/test_helpers-3336601
  • issue/test_helpers-3336801
  • issue/test_helpers-3337449
  • issue/test_helpers-3338212
  • issue/test_helpers-3338294
  • issue/test_helpers-3317747
  • issue/test_helpers-3343675
  • issue/test_helpers-3341228
  • issue/test_helpers-3341297
  • issue/test_helpers-3341353
  • issue/test_helpers-3346191
  • issue/test_helpers-3346933
  • issue/test_helpers-3346992
  • issue/test_helpers-3347151
  • issue/test_helpers-3347857
  • issue/test_helpers-3349366
  • issue/test_helpers-3352012
  • issue/test_helpers-3350134
  • issue/test_helpers-3350342
  • issue/test_helpers-3351234
  • issue/test_helpers-3355114
  • issue/test_helpers-3355369
  • issue/test_helpers-3362966
  • issue/test_helpers-3368325
  • issue/test_helpers-3368326
  • issue/test_helpers-3368329
  • issue/test_helpers-3402316
  • issue/test_helpers-3402809
  • issue/test_helpers-3375838
  • issue/test_helpers-3378383
  • issue/test_helpers-3378433
  • issue/test_helpers-3378437
  • issue/test_helpers-3380037
  • issue/test_helpers-3380615
  • issue/test_helpers-3380729
  • issue/test_helpers-3381225
  • issue/test_helpers-3381364
  • issue/test_helpers-3386688
  • issue/test_helpers-3386710
  • issue/test_helpers-3388157
  • issue/test_helpers-3388492
  • issue/test_helpers-3392105
  • issue/test_helpers-3396940
  • issue/test_helpers-3398074
  • issue/test_helpers-3398386
  • issue/test_helpers-3414071
  • issue/test_helpers-3400489
  • issue/test_helpers-3398264
  • issue/test_helpers-3416420
  • issue/test_helpers-3416423
  • issue/test_helpers-3424496
  • issue/test_helpers-3442210
  • issue/test_helpers-3424298
  • issue/test_helpers-3439635
  • issue/test_helpers-3441740
  • issue/test_helpers-3423972
  • issue/test_helpers-3452070
  • issue/test_helpers-3456140
  • issue/test_helpers-3456164
  • issue/test_helpers-3447490
  • issue/test_helpers-3456208
  • issue/test_helpers-3456236
  • issue/test_helpers-3456245
  • issue/test_helpers-3456398
  • issue/test_helpers-3463033
  • issue/test_helpers-3463927
  • issue/test_helpers-3463989
  • issue/test_helpers-3464490
  • issue/test_helpers-3464617
  • issue/test_helpers-3468855
  • issue/test_helpers-3470642
  • issue/test_helpers-3470644
  • issue/test_helpers-3470930
  • issue/test_helpers-3471164
  • issue/test_helpers-3471384
  • issue/test_helpers-3469602
  • issue/test_helpers-3476348
  • issue/test_helpers-3478014
  • issue/test_helpers-3480281
  • issue/test_helpers-3481649
  • issue/test_helpers-3481744
  • issue/test_helpers-3483212
  • issue/test_helpers-3483795
  • issue/test_helpers-3483807
  • issue/test_helpers-3487152
  • issue/test_helpers-3487396
  • issue/test_helpers-3487503
  • issue/test_helpers-3487474
  • issue/test_helpers-3487678
  • issue/test_helpers-3479414
  • issue/test_helpers-3464623
  • issue/test_helpers-3489526
  • issue/test_helpers-3489704
  • issue/test_helpers-3498161
  • issue/test_helpers-3498168
  • issue/test_helpers-3498792
129 results
Show changes
Commits on Source (209)
Showing
with 642 additions and 684 deletions
# Names.
Alexey
Korepov
Murz
# Project vocabulary
Commercetools
# Tests specific vocabulary.
autoincrement
unmock
unmocks
remocking
todos
# Package names.
donatj
# Non english words to test translations.
histoire
sympa
sympas
coole
geschichte
eine
über
# Placeholders.
foo
bar
baz
qux
quux
corge
grault
garply
waldo
fred
plugh
xyzzy
thud
/docs export-ignore
/.gitlab-ci.yml export-ignore
# These are supported funding model platforms
github: MurzNN # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: MurzNN
# open_collective: # Replace with a single Open Collective username
# ko_fi: # Replace with a single Ko-fi username
tidelift: packagist/drupal/test_helpers
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
# liberapay: # Replace with a single Liberapay username
# issuehunt: # Replace with a single IssueHunt username
# otechie: # Replace with a single Otechie username
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
- https://coindrop.to/murz
- https://www.buymeacoffee.com/murz
*.code-workspace
.phive
.phpdoc
docs
/vendor/
composer.lock
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
rules:
# Process only if we're on the Drupal.org Gitlab instance.
- if: '$_GITLAB_TEMPLATES_REPO == "project/gitlab_templates"'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/includes/include.drupalci.variables.yml
################
# variables:
# SKIP_ESLINT: '1'
# Test multiple PHP versions from the https://git.drupalcode.org/project/keycdn/-/blob/8.x-1.x/.gitlab-ci.yml
#
# Start custom overrides.
#
variables:
# Broaden test coverage.
# Temporary disable previous major because of dropping Drupal 9.x support.
# @todo Enable this when Drupal 11 is released.
# OPT_IN_TEST_PREVIOUS_MAJOR: 1
OPT_IN_TEST_MAX_PHP: 1
OPT_IN_TEST_NEXT_MAJOR: 1
# Convenient, and we have no secrets.
_SHOW_ENVIRONMENT_VARIABLES: 1
_CSPELL_IGNORE_PATHS: "src/lib/CoreFeaturesMaps/**"
pages:
image:
name: phpdoc/phpdoc
entrypoint: [""] # A workaround to force an empty entrypoint
script:
- 'echo "Environment: CI_COMMIT_REF_NAME $CI_COMMIT_REF_NAME CI_COMMIT_BRANCH $CI_COMMIT_BRANCH"'
- phpdoc -t public --title "Drupal Test Helpers module API documentation - release $CI_COMMIT_REF_NAME"
rules:
# Generate documentation only for stable releases tags.
- if: $CI_COMMIT_REF_NAME =~ "/^\d+\.\d+\.\d+$/"
MIT License
Copyright (c) 2023 Alexey Murz Korepov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Test Helpers
Collection of helper classes and functions to use in Drupal Unit tests, Kernel
tests, Functional tests.
The module provides API to simplify writing Drupal tests - unit and functional.
Using the API can significantly reduce the amount of code in your tests to cover
all the logic of tested functions, using provided stubs of Drupal core services
and many other helpers.
## EntityStubFactory
Basically, the module provides stubs for the most popular Drupal services like
Entity Storage, EntityQuery, Database, Configuration Factory, and many others.
The stubs can emulate the behavior of entities (create, load, save, delete) and
core services, but without the real initialization of Drupal Kernel, a database,
and other persistent storages, all are emulated in the memory.
The main helper class is EntityStubFactory which allows quickly create an Entity
Stub in Unit test functions, that will be immediately available to get via
storage functions:
- `EntityStorageInterface::load()`,
- `EntityStorageInterface::loadMultiple()`
- `EntityStorageInterface::loadByProperties()`
- `EntityRepository::loadEntityByUuid()`.
Additionally, it provides some utility functions to get private properties and
methods from classes, Plugin Definitions from a YAML file, and many more.
Here is an example:
Also, it provides helpers for functional and Nightwatch tests, which can
significantly speed up the test execution time.
And to use the Test Helpers API in your project or a contrib module, you don't
even need to install it in Drupal, adding it via composer as a dev dependency
(without installing on production) is enough:
```
composer require --dev 'drupal/test_helpers'
```
- [API Documentation »](https://project.pages.drupalcode.org/test_helpers/)
See the `\Drupal\test_helpers\TestHelpers` class for the main API functions and
read description in PHPDoc comments.
See the `TestHelpers::SERVICES_CUSTOM_STUBS` for the list of the implemented
Drupal core services stubs.
See the `TestHelpers::SERVICES_CORE_INIT` for the list of the Drupal core
services that can be initiated automatically in the unit test context.
See usage examples in the submodule `tests/modules/test_helpers_example` and in
the unit tests in the `tests/src/Unit` directory.
The module also contains submodules to simplify functional and browser testing:
- `test_helpers_functional`: Utilities for functional and Nightwatch tests to
simplify actions on the backend side like creating users, permissions,
switching users, install modules, etc. See the separate README.md file in the
submodule.
- `test_helpers_http_client_mock`: Automates storing the real HTTP responses and
mock them in tests without doing real outgoing HTTP calls. Useful when you use
authorized API calls and don't want to store any credentials in tests. See the
separate README.md file in the submodule.
---
To make all the test submodules available for installing, put this into the
`settings.php` file:
```php
/** use Drupal\test_helpers\EntityStubFactory; */
$entityStubFactory = new EntityStubFactory();
$node1Values = [
'type' => 'article',
'title' => 'My cool article',
'body' => 'Very interesting article text.',
'field_tags' => [
['target_id' => 1],
['target_id' => 3],
],
];
$node1Entity = $entityStubFactory->create(Node::class, $node1Values);
$node1Entity->save();
$node1EntityId = $node1Entity->id();
$node1EntityUuid = $node1Entity->uuid();
$node1EntityType = $node1Entity->getEntityTypeId();
$node1LoadedById = \Drupal::service('entity_type.manager')->getStorage('node')->load($node1EntityId);
$node1LoadedByUuid = \Drupal::service('entity.repository')->loadEntityByUuid($node1EntityType, $node1EntityUuid);
$this->assertEquals(1, $node1LoadedById->id());
$this->assertEquals(1, preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $node1LoadedByUuid->uuid()));
$settings['extension_discovery_scan_tests'] = TRUE;
```
More examples can be found in the unit test file: `tests/src/Unit/EntityStorageStubApiTest.php`.
For the full description of the module, visit the [project
page](https://www.drupal.org/project/test_helpers).
Submit bug reports and feature suggestions, or track changes in the [issue
queue](https://www.drupal.org/project/issues/test_helpers).
Project repository mirrors:
[GitHub](https://github.com/Murz-Drupal/Drupal-Test-Helpers),
[GitLab](https://gitlab.com/murz-drupal/test_helpers).
## Requirements
This module does not have any dependency on any other module.
## Installation
You don't need to install this module to Drupal, just having it as a dev
dependency in `composer` is enough.
## Configuration
## UnitTestHelpers
The module provides no configuration options.
Class `UnitTestHelpers` provides some utility functions:
- `getAccessibleMethod()`: Gets an accessible method from class using reflection.
- `getPluginDefinition()`: Parses the annotation for a Drupal Plugin class and generates a definition.
- `addToContainer()`: Adds a new service to the Drupal container, if exists - reuse existing.
- `getFromContainerOrCreate()`: Gets the service from the Drupal container, or creates a new one.
- `bindClosureToClassMethod()`: Binds a closure function to a mocked class method.
## Maintainers
_It's yet in the early stage of development, so some features are implemented in ugly ways, "just to make them work as needed"._
- Alexey Korepov - [Murz](https://www.drupal.org/u/murz)
{
"name": "drupal/test_helpers",
"description": "Tools and helper functions to simplify writing unit, kernel and functional tests for Drupal",
"type": "drupal-module",
"license": "MIT",
"autoload": {
"psr-4": {
"Drupal\\TestHelpers\\": "src/"
}
},
"authors": [
{
"name": "Alexey Murz Korepov",
"email": "MurzNN@gmail.com"
}
],
"require-dev": {
"donatj/mock-webserver": "^2.7"
}
}
# This is a placeholder file to enable the 'pages' job in Drupal Gitlab.
# The project actually uses phpdoc to generate documentation.
# The configuration file: `phpdoc.xml`.
# A placeholder value to pass eslint error.
foo: bar
<?xml version="1.0" encoding="UTF-8" ?>
<phpdocumentor
configVersion="3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://www.phpdoc.org"
>
<title>Drupal Test Helpers module API documentation</title>
<version number="3.0.0">
<api>
<source dsn=".">
<path>src/TestHelpers.php</path>
<path>src/Stub</path>
<path>src/StubFactory</path>
<path>src/utils</path>
</source>
</api>
</version>
</phpdocumentor>
#!/usr/bin/env php
<?php
/**
* @file
* Generates list of services and entities from the current Drupal Core.
*
* Requires a clean installation of Drupal, without any contrib modules.
*/
use Drupal\Core\DrupalKernel;
use Drupal\Core\Site\Settings;
use Drupal\test_helpers\TestHelpers;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Yaml\Yaml;
// Works only for Core >= 8.9
// For 8.0 - use drupal/drupal instead of drupal/recommended-project and
// drush/drush:^8.
// For versions less than 9.3 - use PHP < 8.1.
// @codingStandardsIgnoreLine
const ONE_LINER = '
export DRUPAL_VERSION=9.4
export ISSUE_ID=3388492
export ISSUE_BRANCH=3388492-parent-services
rm -rf ./drupal_$DRUPAL_VERSION && composer create-project drupal/recommended-project:~$DRUPAL_VERSION.0 drupal_$DRUPAL_VERSION && \
cd drupal_$DRUPAL_VERSION && composer require drush/drush && composer require drupal/test_helpers --prefer-source && ./vendor/bin/drush si --db-url=sqlite://db.sqlite -y && \
cd ./web/modules/contrib/test_helpers && \
git remote add core-features git@git.drupal.org:issue/test_helpers-$ISSUE_ID.git && \
git fetch core-features && \
git checkout core-features/$ISSUE_BRANCH && \
git checkout $ISSUE_BRANCH && \
./scripts/generateCoreFeaturesMap.php && \
git commit -a -m "Drupal $DRUPAL_VERSION services" && \
git push && \
cd ../../../../..
';
$contents = <<<EOT
<?php
/**
* @file
* Pre-generated list of the services from a clean Drupal installation.
*
* This list can be regenerated on a clean Drupal installation using the command
* line script `scripts/generateCoreFeaturesMap.php`.
*/
// @codingStandardsIgnoreFile
EOT;
require_once __DIR__ . '/../src/TestHelpers.php';
$drupalRoot = TestHelpers::getDrupalRoot();
chdir($drupalRoot);
$autoloader = include_once $drupalRoot . '/autoload.php';
require_once $drupalRoot . '/core/includes/bootstrap.inc';
$request = Request::createFromGlobals();
Settings::initialize(dirname(__DIR__, 2), DrupalKernel::findSitePath($request), $autoloader);
DrupalKernel::createFromRequest($request, $autoloader, 'prod')->boot();
\Drupal::service('request_stack')->push($request);
$drupalVersionArray = explode('.', str_replace('-dev', '', \Drupal::VERSION));
$drupalVersionMinor = $drupalVersionArray[0] . '.' . $drupalVersionArray[1];
$filename = dirname(__DIR__) . '/src/lib/CoreFeaturesMaps/CoreFeaturesMap.' . $drupalVersionMinor . '.php';
// Generating services map.
$contents .= <<<EOT
const TEST_HELPERS_DRUPAL_CORE_SERVICE_MAP = [
EOT;
$files = [];
$it = new RecursiveDirectoryIterator($drupalRoot . '/core');
foreach (new RecursiveIteratorIterator($it) as $file) {
if (strpos($file, '/tests/')) {
continue;
}
if (preg_match('#\.services.yml$#', $file)) {
$files[] = $file->getPathName();
}
}
if ($files) {
foreach ($files as $file) {
$data = Yaml::parseFile($file, Yaml::PARSE_CUSTOM_TAGS);
$fileRelative = ltrim(str_replace($drupalRoot, '', $file), '/');
foreach (array_keys($data['services'] ?? []) as $service) {
if (
strpos($service, '\\') !== FALSE
|| $service == '_defaults'
) {
continue;
}
$contents .= <<<EOT
'$service' => '$fileRelative',
EOT;
}
}
}
$contents .= <<<EOT
];
EOT;
// Generating storage map.
$contents .= <<<EOT
const TEST_HELPERS_DRUPAL_CORE_STORAGE_MAP = [
EOT;
$entityTypeManager = \Drupal::service('entity_type.manager');
foreach ($entityTypeManager->getDefinitions() as $type => $definition) {
$class = "'\\" . $definition->getClass() . "'";
$contents .= " '$type' => $class,\n";
}
$contents .= <<<EOT
];
EOT;
// Generating default parameters.
$data = Yaml::parseFile($drupalRoot . '/core/core.services.yml', Yaml::PARSE_CUSTOM_TAGS);
$parametersJson = json_encode($data['parameters']);
$contents .= <<<EOT
const TEST_HELPERS_DRUPAL_CORE_PARAMETERS = '$parametersJson';
EOT;
if (!file_put_contents($filename, $contents)) {
throw new \Exception("Error creating file $filename");
}
echo "Generated services file: $filename" . PHP_EOL;
<?php
namespace Drupal\test_helpers\DataType;
use Drupal\Core\TypedData\PrimitiveBase;
/**
* The item stub data type.
*
* The plain value of a string is a regular PHP string. For setting the value
* any PHP variable that casts to a string may be passed.
*
* @DataType(
* id = "item_stub",
* label = @Translation("Item stub")
* )
*/
class ItemStubData extends PrimitiveBase {
/**
* {@inheritdoc}
*/
public function getCastedValue() {
return $this->getString();
}
}
<?php
namespace Drupal\test_helpers;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Tests\UnitTestCase;
/**
* The Entity Storage Stub class.
*/
class EntityStorageStubFactory extends UnitTestCase {
/**
* Constructs a new EntityStorageStubFactory.
*/
public function __construct() {
$this->entityTypeManager = \Drupal::service('entity_type.manager');
}
/**
* Creates an entity type stub and defines a static storage for it.
*/
public function createInstance(string $entityClass) {
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage|\PHPUnit\Framework\MockObject\MockObject $entityStorageStub */
$entityStorageStub = $this->createPartialMock(SqlContentEntityStorage::class, [
'loadMultiple',
'loadByProperties',
'delete',
/** Custom helper functions for the stub: */
/** Attaches an entity type object to the stub. */
'setEntityType',
/** Generates a next entity id, emulating DB autoincrement behavior. */
'stubGetNewEntityId',
/** Adds or replaces an entity to the static storage. */
'stubAddEntity',
/** Deletes an entity from the static storage. */
'stubDeleteEntity',
]);
$entityType = $this->initEntityDefinition($entityClass);
// @todo Temporary workaround to reuse an already defined property, because
// we've got an error "Indirect modification of overloaded property
// Mock_SqlContentEntityStorage_6202ec22::$stubStorage has no effect" if
// try to use a new own property for this.
$propertyToStoreEntities = 'tableMapping';
$entityStorageStub->stubEntityStorageById = [];
UnitTestHelpers::bindClosureToClassMethod(
function (EntityTypeInterface $entityType) {
$this->entityType = $entityType;
$this->entityTypeId = $entityType->id();
return $this->entityType;
},
$entityStorageStub,
'setEntityType'
);
$entityStorageStub->setEntityType($entityType);
UnitTestHelpers::bindClosureToClassMethod(
function (?array $ids = NULL) use ($propertyToStoreEntities) {
if ($ids === NULL) {
return $this->$propertyToStoreEntities;
}
$entities = [];
foreach ($ids as $id) {
if (isset($this->$propertyToStoreEntities[$id])) {
$entities[$id] = $this->$propertyToStoreEntities[$id];
}
}
return $entities;
},
$entityStorageStub,
'loadMultiple'
);
UnitTestHelpers::bindClosureToClassMethod(
function (array $values = []) use ($propertyToStoreEntities) {
$entities = [];
foreach ($this->$propertyToStoreEntities as $entity) {
foreach ($values as $key => $value) {
// Now getting only the `value` property to compare.
// @todo Try to check the main property and get it.
if (
empty($entity->$key)
|| empty($entity->$key->value)
|| $entity->$key->value != $value
) {
continue 2;
}
}
$entities[] = $entity;
}
return $entities;
},
$entityStorageStub,
'loadByProperties'
);
UnitTestHelpers::bindClosureToClassMethod(
function () use ($propertyToStoreEntities) {
// @todo Make detection of id field type, and calculate only for integers.
$id = max(array_keys($this->$propertyToStoreEntities ?? [0])) + 1;
// The `id` value for even integer autoincrement is stored as string in
// Drupal, so we should follow this behaviour too.
return (string) $id;
},
$entityStorageStub,
'stubGetNewEntityId'
);
UnitTestHelpers::bindClosureToClassMethod(
function ($entity) use ($propertyToStoreEntities) {
$this->$propertyToStoreEntities[$entity->id()] = $entity;
},
$entityStorageStub,
'stubAddEntity'
);
UnitTestHelpers::bindClosureToClassMethod(
function ($entity) use ($propertyToStoreEntities) {
unset($this->$propertyToStoreEntities[$entity->id()]);
},
$entityStorageStub,
'stubDeleteEntity'
);
$this->entitiesStorageById[$entityType->id()] = $entityStorageStub;
return $entityStorageStub;
}
/**
* Initializes an entity definition and adds to storage. Not working yet.
*/
public function initEntityDefinition($entityClass) {
$entityTypeDefinition = UnitTestHelpers::getPluginDefinition($entityClass, 'Entity', '\Drupal\Core\Entity\Annotation\ContentEntityType');
$entityTypeId = $entityTypeDefinition->get('id');
$entityTypeDefinition = $this->entityTypeManager->getDefinition($entityTypeId, FALSE);
if (!$entityTypeDefinition) {
$entityTypeDefinition = UnitTestHelpers::getPluginDefinition($entityClass, 'Entity');
$this->entityTypeManager->stubAddDefinition($entityTypeId, $entityTypeDefinition);
}
return $entityTypeDefinition;
}
}
<?php
namespace Drupal\test_helpers;
use Drupal\Component\Uuid\Php as PhpUuid;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\UnitTestCase;
/**
* The Entity Storage Stub class.
*/
class EntityStubFactory extends UnitTestCase {
/**
* Constructs a new EntityStubFactory.
*/
public function __construct() {
$this->fieldTypeManagerStub = new FieldTypeManagerStub();
$this->fieldItemListStubFactory = new FieldItemListStubFactory($this->fieldTypeManagerStub);
$this->entityTypeManager = (new EntityTypeManagerStubFactory())->create();
$this->typedDataManagerStub = (new TypedDataManagerStubFactory())->createInstance();
UnitTestHelpers::addToContainer('entity.repository', $this->createMock(EntityRepositoryInterface::class));
UnitTestHelpers::addToContainer('entity_field.manager', $this->createMock(EntityFieldManagerInterface::class));
UnitTestHelpers::addToContainer('entity_type.manager', $this->entityTypeManager);
UnitTestHelpers::addToContainer('typed_data_manager', $this->typedDataManagerStub);
UnitTestHelpers::addToContainer('uuid', new PhpUuid());
// Reusing a string field type definition as default one.
$stringItemDefinition = UnitTestHelpers::getPluginDefinition(StringItem::class, 'Field', '\Drupal\Core\Field\Annotation\FieldType');
$this->fieldTypeManagerStub->addDefinition('string', $stringItemDefinition);
/** @var \Drupal\Core\Entity\EntityRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject $entityRepository */
$entityRepository = \Drupal::service('entity.repository');
$entityRepository
->method('loadEntityByUuid')
->willReturnCallback(function ($entityTypeId, $uuid) {
$entityTypeStorage = \Drupal::service('entity_type.manager')->getStorage($entityTypeId);
$uuidProperty = $entityTypeStorage->getEntityType()->getKey('uuid');
return current($entityTypeStorage->loadByProperties([$uuidProperty => $uuid]) ?? []);
});
$entityRepository
->method('getTranslationFromContext')
->will($this->returnArgument(0));
$this->entityStorageStubFactory = new EntityStorageStubFactory();
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface|\PHPUnit\Framework\MockObject\MockObject $entityFieldManager */
$entityFieldManager = \Drupal::service('entity_field.manager');
$entityFieldManager
->method('getFieldDefinitions')
->willReturnCallback(function ($entityTypeId, $bundle) {
// @todo Make a proper return of field definitions.
return [];
});
}
/**
* Creates an entity stub with field values.
*
* @param string $entityClass
* A class path to use when creating the entity.
* @param array $values
* The array of values to set in the created entity.
* @param array $options
* The array of options:
* - methods: the list of additional methods to allow mocking of them.
* - definitions: the list of custom field definitions for needed fields.
* If not passed - the default one (`StringItem`) will be used.
*
* @return \Drupal\Core\Entity\ContentEntityInterface|\PHPUnit\Framework\MockObject\MockObject
* A mocked entity object.
*/
public function create(string $entityClass, array $values = [], array $options = []) {
// Creating a new entity storage stub instance, if not exists.
$storageNew = $this->entityStorageStubFactory->createInstance($entityClass);
$entityTypeDefinition = $storageNew->getEntityType();
$entityTypeId = $storageNew->getEntityTypeId();
$storage = $this->entityTypeManager->stubGetOrCreateStorage($entityTypeId, $storageNew);
// Creating a stub of the entity.
/** @var \Drupal\Core\Entity\ContentEntityInterface|\PHPUnit\Framework\MockObject\MockObject $entity */
$entity = $this->createPartialMock($entityClass, [
'getEntityTypeId',
// 'getFieldDefinitions',
'save',
'delete',
'stubInitValues',
...($options['methods'] ?? []),
]);
// Adding empty values for obligatory fields, if not passed.
foreach ($entityTypeDefinition->get('entity_keys') as $property) {
if (!isset($values[$property])) {
$values[$property] = NULL;
}
}
// Filling values to the entity array.
$fieldItemListStubFactory = $this->fieldItemListStubFactory;
UnitTestHelpers::bindClosureToClassMethod(
function (array $values) use ($fieldItemListStubFactory, $options) {
// Filling common values.
$this->translations[LanguageInterface::LANGCODE_DEFAULT] = ['status' => TRUE];
// Filling values to the entity array.
foreach ($values as $name => $value) {
if (isset($options['definitions'][$name])) {
$definition = $options['definitions'][$name];
}
// @todo Convert entity to TypedDataInterface and pass to the
// item list initialization as a third argument $parent.
$field = $fieldItemListStubFactory->create($name, $value, $definition ?? NULL);
$this->fieldDefinitions[$name] = $field->getFieldDefinition();
$this->fields[$name][LanguageInterface::LANGCODE_DEFAULT] = $field;
}
},
$entity,
'stubInitValues'
);
$entity->stubInitValues($values);
UnitTestHelpers::bindClosureToClassMethod(
function () use ($entityTypeId) {
return $entityTypeId;
},
$entity,
'getEntityTypeId'
);
UnitTestHelpers::bindClosureToClassMethod(
function () use ($storage) {
$idProperty = $this->getEntityType()->getKey('id') ?? NULL;
if ($idProperty && empty($this->$idProperty->value)) {
$this->$idProperty = $storage->stubGetNewEntityId();
}
$uuidProperty = $this->getEntityType()->getKey('uuid') ?? NULL;
if ($uuidProperty && empty($this->$uuidProperty->value)) {
$this->$uuidProperty = \Drupal::service('uuid')->generate();
}
$storage->stubAddEntity($this);
return $this;
},
$entity,
'save'
);
UnitTestHelpers::bindClosureToClassMethod(
function () use ($storage) {
$storage->stubDeleteEntity($this);
},
$entity,
'delete'
);
return $entity;
}
/**
* Returns the Field Type Manager stub.
*/
public function getFieldTypeManagerStub() {
return $this->fieldTypeManagerStub;
}
/**
* Returns the Field Type Manager stub.
*/
public function getFieldItemListStubFactory() {
return $this->fieldItemListStubFactory;
}
/**
* Generates a new entity id, using auto increment like method.
*/
public function generateNewEntityId(string $entityType): string {
// @todo Make detection of id field type, and calculate only for integers.
$id = max(array_keys($this->entitiesStorageById[$entityType] ?? [0])) + 1;
// The `id` value for even integer autoincrement is stored as string in
// Drupal, so we should follow this behaviour too.
return (string) $id;
}
}
<?php
namespace Drupal\test_helpers;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Tests\UnitTestCase;
/**
* The Entity Storage Stub.
*/
class EntityTypeManagerStubFactory extends UnitTestCase {
/**
* Constructs a new FieldTypeManagerStub.
*/
public function create() {
/** @var \Drupal\Core\Entity\EntityTypeManager|\PHPUnit\Framework\MockObject\MockObject $entityTypeManagerStub */
$entityTypeManagerStub = $this->createPartialMock(EntityTypeManager::class, [
'findDefinitions',
/** Custom helper functions for the stub: */
/** Adds a definition to the static storage. */
'stubAddDefinition',
/** Adds or creates a handler. */
'stubGetOrCreateHandler',
/** Adds or creates a storage. */
'stubGetOrCreateStorage',
]);
UnitTestHelpers::bindClosureToClassMethod(
function () {
return [];
},
$entityTypeManagerStub,
'findDefinitions'
);
UnitTestHelpers::bindClosureToClassMethod(
function (string $pluginId, object $definition = NULL, $forceOverride = FALSE) {
if ($forceOverride || !isset($this->definitions[$pluginId])) {
$this->definitions[$pluginId] = $definition;
// $this->handlers['storage'][$pluginId] =
}
return $this->definitions[$pluginId];
},
$entityTypeManagerStub,
'stubAddDefinition'
);
UnitTestHelpers::bindClosureToClassMethod(
function (string $handlerType, string $entityTypeId, object $handler = NULL, $forceOverride = FALSE) {
if ($forceOverride || !isset($this->handlers[$handlerType][$entityTypeId])) {
$this->handlers[$handlerType][$entityTypeId] = $handler;
}
return $this->handlers[$handlerType][$entityTypeId];
},
$entityTypeManagerStub,
'stubGetOrCreateHandler'
);
UnitTestHelpers::bindClosureToClassMethod(
function (string $entityTypeId, object $storage = NULL, $forceOverride = FALSE) {
$storage = $this->stubGetOrCreateHandler('storage', $entityTypeId, $storage);
return $storage;
},
$entityTypeManagerStub,
'stubGetOrCreateStorage'
);
return $entityTypeManagerStub;
}
}
<?php
namespace Drupal\test_helpers;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
use Drupal\Core\TypedData\TypedDataInterface;
use PHPUnit\Framework\TestCase;
/**
* The FieldItemList Stub factory.
*/
class FieldItemListStubFactory extends TestCase {
/**
* The Field Type Manager stub.
*
* @var \Drupal\test_helpers\FieldTypeManagerStub
*/
protected $fieldTypeManagerStub;
/**
* Constructs a new EntityStubFactory.
*
* @param \Drupal\test_helpers\FieldTypeManagerStub $fieldTypeManagerStub
* The Field Type Manager stub to use.
*/
public function __construct(FieldTypeManagerStub $fieldTypeManagerStub) {
$this->fieldTypeManagerStub = $fieldTypeManagerStub;
}
/**
* Creates a field definition stub.
*
* @param string $name
* Field name.
* @param string $type
* Type of the creating field.
* @param string $class
* The class name to use for item definition.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* A field field definition stub.
*/
public function createFieldItemDefinitionStub(string $name, string $type, string $class = NULL): FieldDefinitionInterface {
if (!$class) {
$class = StringItem::class;
}
// @todo Now it's a quick initialization of BaseFieldDefinition,
// will be good to add support for other field types.
$field_definition = BaseFieldDefinition::create($type);
$field_definition->setName($name);
$field_definition->getItemDefinition()->setClass($class);
return $field_definition;
}
/**
* Creates an entity type stub and defines a static storage for it.
*
* @param string $name
* Field name.
* @param mixed $values
* Field values array.
* @param \Drupal\Core\Field\FieldDefinitionInterface $definition
* Definition to use, will use BaseFieldDefinition if not passed.
* @param \Drupal\Core\TypedData\TypedDataInterface $parent
* Parent item for attaching to the field.
*
* @return \Drupal\Core\Field\FieldItemListInterface
* A field item list with items as stubs.
*/
public function create(string $name, $values = NULL, FieldDefinitionInterface $definition = NULL, TypedDataInterface $parent = NULL): FieldItemListInterface {
if (!$definition) {
// @todo Now it's a hard-coded type, will be good to add support for
// custom types.
$type = 'string';
$definition = $this->createFieldItemDefinitionStub($name, $type, StringItem::class);
}
$field = new FieldItemList($definition, $name, $parent);
$field->setValue($values);
return $field;
}
}
<?php
namespace Drupal\test_helpers;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Tests\UnitTestCase;
/**
* The Entity Storage Stub.
*/
class FieldTypeManagerStub extends UnitTestCase {
/**
* Static storage for defined definitions.
*
* @var array
*/
protected $definitions;
/**
* Mapping of field item classes by list class.
*
* @var array
*/
protected $fieldItemClassByListClassMap;
/**
* Constructs a new FieldTypeManagerStub.
*/
public function __construct() {
$this->fieldItemClassByListClassMap = [];
$fieldTypePluginManager = UnitTestHelpers::addToContainer('plugin.manager.field.field_type', $this->createMock(FieldTypePluginManagerInterface::class));
$fieldTypePluginManager
->method('getDefinitions')
->willReturnCallback(function () {
return $this->definitions;
});
$fieldTypePluginManager
->method('getDefinition')
->willReturnCallback(function ($type) {
return $this->definitions[$type];
});
$fieldTypePluginManager
->method('getDefaultStorageSettings')
->willReturnCallback(function ($type) {
return $this->definitions[$type]['storage_settings'] ?? [];
});
$fieldTypePluginManager
->method('getDefaultFieldSettings')
->willReturnCallback(function ($type) {
return $this->definitions[$type]['field_settings'] ?? [];
});
$fieldTypePluginManager
->method('createFieldItem')
->willReturnCallback(function ($items, $index, $values) {
$itemClass = $items->getFieldDefinition()->getItemDefinition()->getClass();
foreach ($this->fieldItemClassByListClassMap as $listClass => $itemClassCandidate) {
if ($items instanceof $listClass) {
$itemClass = $itemClassCandidate;
break;
}
}
$fieldItemDefinition = $items->getFieldDefinition()->getItemDefinition();
// Using field item definition, if exists.
if (is_object($fieldItemDefinition)) {
$fieldItemDefinition->setClass($itemClass);
$fieldItem = new $itemClass($fieldItemDefinition);
}
// If field definition is not defined, creating a mock for it.
// @todo Make it better.
else {
$propertyDefinitions['value'] = $this->createMock(DataDefinitionInterface::class);
$propertyDefinitions['value']->expects($this->any())
->method('isComputed')
->willReturn(FALSE);
$fieldDefinition = $this->createMock(BaseFieldDefinition::class);
$fieldDefinition->expects($this->any())
->method('getPropertyDefinitions')
->willReturn($this->returnValue($propertyDefinitions));
$fieldInstanceDefinition = $this->createMock(FieldItemDataDefinitionInterface::class);
$fieldInstanceDefinition->expects($this->any())
->method('getPropertyDefinitions')
->willReturn($this->returnValue($propertyDefinitions));
$fieldInstanceDefinition->expects($this->any())
->method('getFieldDefinition')
->willReturn($this->returnValue($fieldDefinition));
$fieldItem = new $itemClass($fieldInstanceDefinition);
}
// Applying the value to the field item.
$fieldItem->setValue($values);
return $fieldItem;
});
}
/**
* Adds the definition, to the static storage.
*/
public function addDefinition(string $fieldType, $definition = []) {
if (!isset($definition['id'])) {
$definition['id'] = $fieldType;
}
$this->definitions[$fieldType] = $definition;
}
/**
* Defines the mapping of list class with field item class.
*/
public function defineFieldItemClassByListClass(string $listClass, string $itemClass) {
$this->fieldItemClassByListClassMap[$listClass] = $itemClass;
}
}
<?php
namespace Drupal\test_helpers\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
/**
* A stub for Drupal's default FieldItemBase class.
*
* @FieldType(
* id = "item_stub",
* label = "Item Stub",
* description = "A stub field item to simplify writing unit tests.",
* category = "test_helpers",
* default_widget = "string_textfield",
* default_formatter = "string"
* )
*/
class ItemStubItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $fieldDefinition) {
return [
'columns' => [
'value' => [
'type' => $fieldDefinition->getSetting('is_ascii') === TRUE ? 'varchar_ascii' : 'varchar',
'length' => (int) $fieldDefinition->getSetting('max_length'),
'binary' => $fieldDefinition->getSetting('case_sensitive'),
],
],
];
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
// This is called very early by the user entity roles field. Prevent
// early t() calls by using the TranslatableMarkup.
$properties['value'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Text value'))
->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'))
->setRequired(TRUE);
return $properties;
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'value';
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
foreach ($this->values ?? [] as $value) {
if (!empty($value)) {
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$random = new Random();
$values['value'] = $random->word(mt_rand(1, $field_definition->getSetting('max_length')));
return $values;
}
}
<?php
namespace Drupal\test_helpers\Stub;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A stub of the Drupal's default ConfigFactory class.
*
* Validates any context names by default, until they are not defined by the
* function stubAddContext().
*
* @package TestHelpers\DrupalServiceStubs
*/
class CacheContextsManagerStub extends CacheContextsManager {
/**
* A flag to accept all contexts by default, if no contexts is set manually.
*
* @var bool
*/
protected $stubAllowAnyContexts;
/**
* Constructs a CacheContextsManager object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The current service container.
* @param string[] $contexts
* An array of the available cache context IDs, NULL to accept all contexts.
*/
public function __construct(ContainerInterface $container, ?array $contexts = NULL) {
parent::__construct($container, $contexts ?? []);
$this->stubAllowAnyContexts = TRUE;
}
/**
* Adds contexts to the valid list.
*
* @param string|string[] $contexts
* The list of valid contexts.
*/
public function stubAddContexts($contexts) {
$this->stubAllowAnyContexts = FALSE;
if (is_string($contexts)) {
$contexts = [$contexts];
}
$this->contexts = array_merge($this->contexts, $contexts);
unset($this->validContextTokens);
}
/**
* Sets the full list of contexts.
*
* @param string[] $contexts
* The list of valid contexts.
*/
public function stubSetContexts(array $contexts) {
$this->stubAllowAnyContexts = FALSE;
$this->contexts = $contexts;
unset($this->validContextTokens);
}
/**
* {@inheritdoc}
*/
public function assertValidTokens($context_tokens) {
if ($this->stubAllowAnyContexts === TRUE) {
return TRUE;
}
return parent::assertValidTokens($context_tokens);
}
}
<?php
namespace Drupal\test_helpers\Stub;
use Drupal\Core\Cache\CacheFactory;
use Drupal\Core\Site\Settings;
use Drupal\test_helpers\TestHelpers;
/**
* A stub of the Drupal's core CacheFactory class.
*
* @package TestHelpers\DrupalServiceStubs
*/
class CacheFactoryStub extends CacheFactory {
/**
* {@inheritdoc}
*/
public function __construct(Settings $settings, array $default_bin_backends = []) {
$cacheSettings = $settings->get('cache');
if (isset($cacheSettings['default'])) {
TestHelpers::service($cacheSettings['default']);
}
parent::__construct($settings, $default_bin_backends);
}
}