3 namespace Drupal\simpletest;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Render\FormattableMarkup;
7 use Drupal\Component\Utility\Variable;
8 use Drupal\Core\Config\Development\ConfigSchemaChecker;
9 use Drupal\Core\Database\Database;
10 use Drupal\Core\DependencyInjection\ContainerBuilder;
11 use Drupal\Core\DrupalKernel;
12 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
13 use Drupal\Core\Extension\ExtensionDiscovery;
14 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
15 use Drupal\Core\Language\Language;
16 use Drupal\Core\Site\Settings;
17 use Drupal\KernelTests\TestServiceProvider;
18 use Symfony\Component\DependencyInjection\Parameter;
19 use Drupal\Core\StreamWrapper\StreamWrapperInterface;
20 use Symfony\Component\DependencyInjection\Reference;
21 use Symfony\Component\HttpFoundation\Request;
24 * Base class for functional integration tests.
26 * This base class should be useful for testing some types of integrations which
27 * don't require the overhead of a fully-installed Drupal instance, but which
28 * have many dependencies on parts of Drupal which can't or shouldn't be mocked.
30 * This base class partially boots a fixture Drupal. The state of the fixture
31 * Drupal is comparable to the state of a system during the early part of the
32 * installation process.
34 * Tests extending this base class can access services and the database, but the
35 * system is initially empty. This Drupal runs in a minimal mocked filesystem
36 * which operates within vfsStream.
38 * Modules specified in the $modules property are added to the service container
39 * for each test. The module/hook system is functional. Additional modules
40 * needed in a test should override $modules. Modules specified in this way will
41 * be added to those specified in superclasses.
43 * Unlike \Drupal\Tests\BrowserTestBase, the modules are not installed. They are
44 * loaded such that their services and hooks are available, but the install
45 * process has not been performed.
47 * Other modules can be made available in this way using
48 * KernelTestBase::enableModules().
50 * Some modules can be brought into a fully-installed state using
51 * KernelTestBase::installConfig(), KernelTestBase::installSchema(), and
52 * KernelTestBase::installEntitySchema(). Alternately, tests which need modules
53 * to be fully installed could inherit from \Drupal\Tests\BrowserTestBase.
55 * @see \Drupal\Tests\KernelTestBase::$modules
56 * @see \Drupal\Tests\KernelTestBase::enableModules()
57 * @see \Drupal\Tests\KernelTestBase::installConfig()
58 * @see \Drupal\Tests\KernelTestBase::installEntitySchema()
59 * @see \Drupal\Tests\KernelTestBase::installSchema()
60 * @see \Drupal\Tests\BrowserTestBase
62 * @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0. Use
63 * \Drupal\KernelTests\KernelTestBase instead.
67 abstract class KernelTestBase extends TestBase {
69 use AssertContentTrait;
74 * Test classes extending this class, and any classes in the hierarchy up to
75 * this class, may specify individual lists of modules to enable by setting
76 * this property. The values of all properties in all classes in the hierarchy
79 * Any modules specified in the $modules property are automatically loaded and
80 * set as the fixed module list.
82 * Unlike WebTestBase::setUp(), the specified modules are loaded only, but not
83 * automatically installed. Modules need to be installed manually, if needed.
85 * @see \Drupal\simpletest\KernelTestBase::enableModules()
86 * @see \Drupal\simpletest\KernelTestBase::setUp()
90 public static $modules = [];
96 * The configuration directories for this test run.
100 protected $configDirectories = [];
103 * A KeyValueMemoryFactory instance to use when building the container.
105 * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory
107 protected $keyValueFactory;
110 * Array of registered stream wrappers.
114 protected $streamWrappers = [];
119 public function __construct($test_id = NULL) {
120 parent::__construct($test_id);
121 $this->skipClasses[__CLASS__] = TRUE;
127 protected function beforePrepareEnvironment() {
128 // Copy/prime extension file lists once to avoid filesystem scans.
129 if (!isset($this->moduleFiles)) {
130 $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: [];
131 $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: [];
136 * Create and set new configuration directories.
138 * @see config_get_config_directory()
140 * @throws \RuntimeException
141 * Thrown when CONFIG_SYNC_DIRECTORY cannot be created or made writable.
143 protected function prepareConfigDirectories() {
144 $this->configDirectories = [];
145 include_once DRUPAL_ROOT . '/core/includes/install.inc';
146 // Assign the relative path to the global variable.
147 $path = $this->siteDirectory . '/config_' . CONFIG_SYNC_DIRECTORY;
148 $GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY] = $path;
149 // Ensure the directory can be created and is writeable.
150 if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
151 throw new \RuntimeException("Failed to create '" . CONFIG_SYNC_DIRECTORY . "' config directory $path");
153 // Provide the already resolved path for tests.
154 $this->configDirectories[CONFIG_SYNC_DIRECTORY] = $path;
160 protected function setUp() {
161 $this->keyValueFactory = new KeyValueMemoryFactory();
163 // Back up settings from TestBase::prepareEnvironment().
164 $settings = Settings::getAll();
166 // Allow for test-specific overrides.
167 $directory = DRUPAL_ROOT . '/' . $this->siteDirectory;
168 $settings_services_file = DRUPAL_ROOT . '/' . $this->originalSite . '/testing.services.yml';
169 $container_yamls = [];
170 if (file_exists($settings_services_file)) {
171 // Copy the testing-specific service overrides in place.
172 $testing_services_file = $directory . '/services.yml';
173 copy($settings_services_file, $testing_services_file);
174 $container_yamls[] = $testing_services_file;
176 $settings_testing_file = DRUPAL_ROOT . '/' . $this->originalSite . '/settings.testing.php';
177 if (file_exists($settings_testing_file)) {
178 // Copy the testing-specific settings.php overrides in place.
179 copy($settings_testing_file, $directory . '/settings.testing.php');
182 if (file_exists($directory . '/settings.testing.php')) {
183 // Add the name of the testing class to settings.php and include the
184 // testing specific overrides
185 $hash_salt = Settings::getHashSalt();
186 $test_class = get_class($this);
187 $container_yamls_export = Variable::export($container_yamls);
191 \$settings['hash_salt'] = '$hash_salt';
192 \$settings['container_yamls'] = $container_yamls_export;
194 \$test_class = '$test_class';
195 include DRUPAL_ROOT . '/' . \$site_path . '/settings.testing.php';
197 file_put_contents($directory . '/settings.php', $php);
200 // Add this test class as a service provider.
201 // @todo Remove the indirection; implement ServiceProviderInterface instead.
202 $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = TestServiceProvider::class;
204 // Bootstrap a new kernel.
205 $class_loader = require DRUPAL_ROOT . '/autoload.php';
206 $this->kernel = new DrupalKernel('testing', $class_loader, FALSE);
207 $request = Request::create('/');
208 $site_path = DrupalKernel::findSitePath($request);
209 $this->kernel->setSitePath($site_path);
210 if (file_exists($directory . '/settings.testing.php')) {
211 Settings::initialize(DRUPAL_ROOT, $site_path, $class_loader);
213 $this->kernel->boot();
215 // Ensure database install tasks have been run.
216 require_once __DIR__ . '/../../../includes/install.inc';
217 $connection = Database::getConnection();
218 $errors = db_installer_object($connection->driver())->runTasks();
219 if (!empty($errors)) {
220 $this->fail('Failed to run installer database tasks: ' . implode(', ', $errors));
223 // Reboot the kernel because the container might contain a connection to the
224 // database that has been closed during the database install tasks. This
225 // prevents any services created during the first boot from having stale
226 // database connections, for example, \Drupal\Core\Config\DatabaseStorage.
227 $this->kernel->shutdown();
228 $this->kernel->boot();
230 // Save the original site directory path, so that extensions in the
231 // site-specific directory can still be discovered in the test site
233 // @see \Drupal\Core\Extension\ExtensionDiscovery::scan()
234 $settings['test_parent_site'] = $this->originalSite;
236 // Restore and merge settings.
237 // DrupalKernel::boot() initializes new Settings, and the containerBuild()
238 // method sets additional settings.
239 new Settings($settings + Settings::getAll());
241 // Create and set new configuration directories.
242 $this->prepareConfigDirectories();
244 // Set the request scope.
245 $this->container = $this->kernel->getContainer();
246 $this->container->get('request_stack')->push($request);
248 // Re-inject extension file listings into state, unless the key/value
249 // service was overridden (in which case its storage does not exist yet).
250 if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) {
251 $this->container->get('state')->set('system.module.files', $this->moduleFiles);
252 $this->container->get('state')->set('system.theme.files', $this->themeFiles);
255 // Create a minimal core.extension configuration object so that the list of
256 // enabled modules can be maintained allowing
257 // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
258 // Write directly to active storage to avoid early instantiation of
259 // the event dispatcher which can prevent modules from registering events.
260 \Drupal::service('config.storage')->write('core.extension', ['module' => [], 'theme' => [], 'profile' => '']);
262 // Collect and set a fixed module list.
263 $class = get_class($this);
266 if (property_exists($class, 'modules')) {
267 // Only add the modules, if the $modules property was not inherited.
268 $rp = new \ReflectionProperty($class, 'modules');
269 if ($rp->class == $class) {
270 $modules[$class] = $class::$modules;
273 $class = get_parent_class($class);
275 // Modules have been collected in reverse class hierarchy order; modules
276 // defined by base classes should be sorted first. Then, merge the results
278 $modules = array_reverse($modules);
279 $modules = call_user_func_array('array_merge_recursive', $modules);
281 $this->enableModules($modules);
284 // Tests based on this class are entitled to use Drupal's File and
285 // StreamWrapper APIs.
286 // @todo Move StreamWrapper management into DrupalKernel.
287 // @see https://www.drupal.org/node/2028109
288 file_prepare_directory($this->publicFilesDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
289 $this->settingsSet('file_public_path', $this->publicFilesDirectory);
290 $this->streamWrappers = [];
291 $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream');
292 // The temporary stream wrapper is able to operate both with and without
294 $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
296 // Manually configure the test mail collector implementation to prevent
297 // tests from sending out emails and collect them in state instead.
298 // While this should be enforced via settings.php prior to installation,
299 // some tests expect to be able to test mail system implementations.
300 $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector';
306 protected function tearDown() {
307 if ($this->kernel instanceof DrupalKernel) {
308 $this->kernel->shutdown();
310 // Before tearing down the test environment, ensure that no stream wrapper
311 // of this test leaks into the parent environment. Unlike all other global
312 // state variables in Drupal, stream wrappers are a global state construct
313 // of PHP core, which has to be maintained manually.
314 // @todo Move StreamWrapper management into DrupalKernel.
315 // @see https://www.drupal.org/node/2028109
316 foreach ($this->streamWrappers as $scheme => $type) {
317 $this->unregisterStreamWrapper($scheme, $type);
323 * Sets up the base service container for this test.
325 * Extend this method in your test to register additional service overrides
326 * that need to persist a DrupalKernel reboot. This method is called whenever
327 * the kernel is rebuilt.
329 * @see \Drupal\simpletest\KernelTestBase::setUp()
330 * @see \Drupal\simpletest\KernelTestBase::enableModules()
331 * @see \Drupal\simpletest\KernelTestBase::disableModules()
333 public function containerBuild(ContainerBuilder $container) {
334 // Keep the container object around for tests.
335 $this->container = $container;
337 // Set the default language on the minimal container.
338 $this->container->setParameter('language.default_values', $this->defaultLanguageData());
340 $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
341 $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
344 ->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
345 ->addArgument(Database::getConnection())
346 ->addArgument('config');
348 if ($this->strictConfigSchema) {
350 ->register('simpletest.config_schema_checker', ConfigSchemaChecker::class)
351 ->addArgument(new Reference('config.typed'))
352 ->addArgument($this->getConfigSchemaExclusions())
353 ->addTag('event_subscriber');
356 $keyvalue_options = $container->getParameter('factory.keyvalue') ?: [];
357 $keyvalue_options['default'] = 'keyvalue.memory';
358 $container->setParameter('factory.keyvalue', $keyvalue_options);
359 $container->set('keyvalue.memory', $this->keyValueFactory);
360 if (!$container->has('keyvalue')) {
361 // TestBase::setUp puts a completely empty container in
362 // $this->container which is somewhat the mirror of the empty
363 // environment being set up. Unit tests need not to waste time with
364 // getting a container set up for them. Drupal Unit Tests might just get
365 // away with a simple container holding the absolute bare minimum. When
366 // a kernel is overridden then there's no need to re-register the keyvalue
367 // service but when a test is happy with the superminimal container put
368 // together here, it still might a keyvalue storage for anything using
369 // \Drupal::state() -- that's why a memory service was added in the first
371 $container->register('settings', 'Drupal\Core\Site\Settings')
372 ->setFactoryClass('Drupal\Core\Site\Settings')
373 ->setFactoryMethod('getInstance');
376 ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
377 ->addArgument(new Reference('service_container'))
378 ->addArgument(new Parameter('factory.keyvalue'));
380 $container->register('state', 'Drupal\Core\State\State')
381 ->addArgument(new Reference('keyvalue'));
384 if ($container->hasDefinition('path_processor_alias')) {
385 // Prevent the alias-based path processor, which requires a url_alias db
386 // table, from being registered to the path processor manager. We do this
387 // by removing the tags that the compiler pass looks for. This means the
388 // url generator can safely be used within tests.
389 $definition = $container->getDefinition('path_processor_alias');
390 $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
393 if ($container->hasDefinition('password')) {
394 $container->getDefinition('password')->setArguments([1]);
397 // Register the stream wrapper manager.
399 ->register('stream_wrapper_manager', 'Drupal\Core\StreamWrapper\StreamWrapperManager')
400 ->addArgument(new Reference('module_handler'))
401 ->addMethodCall('setContainer', [new Reference('service_container')]);
403 $request = Request::create('/');
404 $container->get('request_stack')->push($request);
408 * Provides the data for setting the default language on the container.
411 * The data array for the default language.
413 protected function defaultLanguageData() {
414 return Language::$defaultValues;
418 * Installs default configuration for a given list of modules.
420 * @param array $modules
421 * A list of modules for which to install default configuration.
423 * @throws \RuntimeException
424 * Thrown when any module listed in $modules is not enabled.
426 protected function installConfig(array $modules) {
427 foreach ($modules as $module) {
428 if (!$this->container->get('module_handler')->moduleExists($module)) {
429 throw new \RuntimeException("'$module' module is not enabled");
431 \Drupal::service('config.installer')->installDefaultConfig('module', $module);
433 $this->pass(format_string('Installed default config: %modules.', [
434 '%modules' => implode(', ', $modules),
439 * Installs a specific table from a module schema definition.
441 * @param string $module
442 * The name of the module that defines the table's schema.
443 * @param string|array $tables
444 * The name or an array of the names of the tables to install.
446 * @throws \RuntimeException
447 * Thrown when $module is not enabled or when the table schema cannot be
448 * found in the module specified.
450 protected function installSchema($module, $tables) {
451 // drupal_get_module_schema() is technically able to install a schema
452 // of a non-enabled module, but its ability to load the module's .install
453 // file depends on many other factors. To prevent differences in test
454 // behavior and non-reproducible test failures, we only allow the schema of
455 // explicitly loaded/enabled modules to be installed.
456 if (!$this->container->get('module_handler')->moduleExists($module)) {
457 throw new \RuntimeException("'$module' module is not enabled");
460 $tables = (array) $tables;
461 foreach ($tables as $table) {
462 $schema = drupal_get_module_schema($module, $table);
463 if (empty($schema)) {
464 // BC layer to avoid some contrib tests to fail.
465 // @todo Remove the BC layer before 8.1.x release.
466 // @see https://www.drupal.org/node/2670360
467 // @see https://www.drupal.org/node/2670454
468 if ($module == 'system') {
471 throw new \RuntimeException("Unknown '$table' table schema in '$module' module.");
473 $this->container->get('database')->schema()->createTable($table, $schema);
475 $this->pass(format_string('Installed %module tables: %tables.', [
476 '%tables' => '{' . implode('}, {', $tables) . '}',
477 '%module' => $module,
482 * Installs the storage schema for a specific entity type.
484 * @param string $entity_type_id
485 * The ID of the entity type.
487 protected function installEntitySchema($entity_type_id) {
488 /** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
489 $entity_manager = $this->container->get('entity.manager');
490 $entity_type = $entity_manager->getDefinition($entity_type_id);
491 $entity_manager->onEntityTypeCreate($entity_type);
493 // For test runs, the most common storage backend is a SQL database. For
494 // this case, ensure the tables got created.
495 $storage = $entity_manager->getStorage($entity_type_id);
496 if ($storage instanceof SqlEntityStorageInterface) {
497 $tables = $storage->getTableMapping()->getTableNames();
498 $db_schema = $this->container->get('database')->schema();
499 $all_tables_exist = TRUE;
500 foreach ($tables as $table) {
501 if (!$db_schema->tableExists($table)) {
502 $this->fail(new FormattableMarkup('Installed entity type table for the %entity_type entity type: %table', [
503 '%entity_type' => $entity_type_id,
506 $all_tables_exist = FALSE;
509 if ($all_tables_exist) {
510 $this->pass(new FormattableMarkup('Installed entity type tables for the %entity_type entity type: %tables', [
511 '%entity_type' => $entity_type_id,
512 '%tables' => '{' . implode('}, {', $tables) . '}',
519 * Enables modules for this test.
521 * To install test modules outside of the testing environment, add
523 * $settings['extension_discovery_scan_tests'] = TRUE;
525 * to your settings.php.
527 * @param array $modules
528 * A list of modules to enable. Dependencies are not resolved; i.e.,
529 * multiple modules have to be specified with dependent modules first.
530 * The new modules are only added to the active module list and loaded.
532 protected function enableModules(array $modules) {
533 // Perform an ExtensionDiscovery scan as this function may receive a
534 // profile that is not the current profile, and we don't yet have a cached
535 // way to receive inactive profile information.
536 // @todo Remove as part of https://www.drupal.org/node/2186491
537 $listing = new ExtensionDiscovery(\Drupal::root());
538 $module_list = $listing->scan('module');
539 // In ModuleHandlerTest we pass in a profile as if it were a module.
540 $module_list += $listing->scan('profile');
541 // Set the list of modules in the extension handler.
542 $module_handler = $this->container->get('module_handler');
544 // Write directly to active storage to avoid early instantiation of
545 // the event dispatcher which can prevent modules from registering events.
546 $active_storage = \Drupal::service('config.storage');
547 $extensions = $active_storage->read('core.extension');
549 foreach ($modules as $module) {
550 $module_handler->addModule($module, $module_list[$module]->getPath());
551 // Maintain the list of enabled modules in configuration.
552 $extensions['module'][$module] = 0;
554 $active_storage->write('core.extension', $extensions);
556 // Update the kernel to make their services available.
557 $module_filenames = $module_handler->getModuleList();
558 $this->kernel->updateModules($module_filenames, $module_filenames);
560 // Ensure isLoaded() is TRUE in order to make
561 // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
562 // Note that the kernel has rebuilt the container; this $module_handler is
563 // no longer the $module_handler instance from above.
564 $this->container->get('module_handler')->reload();
565 $this->pass(format_string('Enabled modules: %modules.', [
566 '%modules' => implode(', ', $modules),
571 * Disables modules for this test.
573 * @param array $modules
574 * A list of modules to disable. Dependencies are not resolved; i.e.,
575 * multiple modules have to be specified with dependent modules first.
576 * Code of previously active modules is still loaded. The modules are only
577 * removed from the active module list.
579 protected function disableModules(array $modules) {
580 // Unset the list of modules in the extension handler.
581 $module_handler = $this->container->get('module_handler');
582 $module_filenames = $module_handler->getModuleList();
583 $extension_config = $this->config('core.extension');
584 foreach ($modules as $module) {
585 unset($module_filenames[$module]);
586 $extension_config->clear('module.' . $module);
588 $extension_config->save();
589 $module_handler->setModuleList($module_filenames);
590 $module_handler->resetImplementations();
591 // Update the kernel to remove their services.
592 $this->kernel->updateModules($module_filenames, $module_filenames);
594 // Ensure isLoaded() is TRUE in order to make
595 // \Drupal\Core\Theme\ThemeManagerInterface::render() work.
596 // Note that the kernel has rebuilt the container; this $module_handler is
597 // no longer the $module_handler instance from above.
598 $module_handler = $this->container->get('module_handler');
599 $module_handler->reload();
600 $this->pass(format_string('Disabled modules: %modules.', [
601 '%modules' => implode(', ', $modules),
606 * Registers a stream wrapper for this test.
608 * @param string $scheme
609 * The scheme to register.
610 * @param string $class
611 * The fully qualified class name to register.
613 * The Drupal Stream Wrapper API type. Defaults to
614 * StreamWrapperInterface::NORMAL.
616 protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperInterface::NORMAL) {
617 $this->container->get('stream_wrapper_manager')->registerWrapper($scheme, $class, $type);
621 * Renders a render array.
623 * @param array $elements
624 * The elements to render.
627 * The rendered string output (typically HTML).
629 protected function render(array &$elements) {
630 // Use the bare HTML page renderer to render our links.
631 $renderer = $this->container->get('bare_html_page_renderer');
632 $response = $renderer->renderBarePage($elements, '', 'maintenance_page');
634 // Glean the content from the response object.
635 $content = $response->getContent();
636 $this->setRawContent($content);
637 $this->verbose('<pre style="white-space: pre-wrap">' . Html::escape($content));