DrupalUnitTestBase.php 18 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\simpletest\DrupalUnitTestBase.
6 7 8 9
 */

namespace Drupal\simpletest;

10
use Drupal\Core\DependencyInjection\ContainerBuilder;
11
use Drupal\Core\DrupalKernel;
12
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
13
use Drupal\Core\Language\Language;
14
use Symfony\Component\DependencyInjection\Reference;
15
use Drupal\Core\Database\Database;
16
use Symfony\Component\DependencyInjection\ContainerInterface;
17
use Symfony\Component\HttpFoundation\Request;
18 19 20 21 22 23 24 25 26 27 28 29

/**
 * Base test case class for Drupal unit tests.
 *
 * Tests extending this base class can access files and the database, but the
 * entire environment is initially empty. Drupal runs in a minimal mocked
 * environment, comparable to the one in the installer or update.php.
 *
 * The module/hook system is functional and operates on a fixed module list.
 * Additional modules needed in a test may be loaded and added to the fixed
 * module list.
 *
30 31
 * @see \DrupalUnitTestBase::$modules
 * @see \DrupalUnitTestBase::enableModules()
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
 */
abstract class DrupalUnitTestBase extends UnitTestBase {

  /**
   * Modules to enable.
   *
   * Test classes extending this class, and any classes in the hierarchy up to
   * this class, may specify individual lists of modules to enable by setting
   * this property. The values of all properties in all classes in the hierarchy
   * are merged.
   *
   * Unlike UnitTestBase::setUp(), any modules specified in the $modules
   * property are automatically loaded and set as the fixed module list.
   *
   * Unlike WebTestBase::setUp(), the specified modules are loaded only, but not
   * automatically installed. Modules need to be installed manually, if needed.
   *
49 50
   * @see \DrupalUnitTestBase::enableModules()
   * @see \DrupalUnitTestBase::setUp()
51 52 53 54 55 56 57 58 59
   *
   * @var array
   */
  public static $modules = array();

  private $moduleFiles;
  private $themeFiles;
  private $themeData;

60 61 62 63 64 65 66
  /**
   * The configuration directories for this test run.
   *
   * @var array
   */
  protected $configDirectories = array();

67 68 69 70 71 72 73
  /**
   * A KeyValueMemoryFactory instance to use when building the container.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
   */
  protected $keyValueFactory;

74 75 76 77 78 79 80 81 82
  /**
   * A list of stream wrappers that have been registered for this test.
   *
   * @see \Drupal\simpletest\DrupalUnitTestBase::registerStreamWrapper()
   *
   * @var array
   */
  private $streamWrappers = array();

83 84 85 86 87 88 89 90
  /**
   * Overrides \Drupal\simpletest\UnitTestBase::__construct().
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

91
  /**
92
   * Overrides TestBase::beforePrepareEnvironment().
93
   */
94
  protected function beforePrepareEnvironment() {
95 96
    // Copy/prime extension file lists once to avoid filesystem scans.
    if (!isset($this->moduleFiles)) {
97 98 99
      $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: array();
      $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: array();
      $this->themeData = \Drupal::state()->get('system.theme.data') ?: array();
100
    }
101
  }
102

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
  /**
   * Create and set new configuration directories.
   *
   * @see config_get_config_directory()
   */
  protected function prepareConfigDirectories() {
    $this->configDirectories = array();
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
    foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) {
      // Assign the relative path to the global variable.
      $path = $this->siteDirectory . '/config_' . $type;
      $GLOBALS['config_directories'][$type] = $path;
      // Ensure the directory can be created and is writeable.
      if (!install_ensure_config_directory($type)) {
        throw new \RuntimeException("Failed to create '$type' config directory $path");
      }
      // Provide the already resolved path for tests.
      $this->configDirectories[$type] = $path;
    }
  }

124 125 126 127
  /**
   * Sets up Drupal unit test environment.
   */
  protected function setUp() {
128 129
    $this->keyValueFactory = new KeyValueMemoryFactory();

130
    parent::setUp();
131

132 133 134
    // Create and set new configuration directories.
    $this->prepareConfigDirectories();

135
    // Build a minimal, partially mocked environment for unit tests.
136
    $this->containerBuild(\Drupal::getContainer());
137
    // Make sure it survives kernel rebuilds.
138
    $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = 'Drupal\simpletest\TestServiceProvider';
139

140 141 142
    \Drupal::state()->set('system.module.files', $this->moduleFiles);
    \Drupal::state()->set('system.theme.files', $this->themeFiles);
    \Drupal::state()->set('system.theme.data', $this->themeData);
143

144
    // Bootstrap the kernel.
145
    // No need to dump it; this test runs in-memory.
146
    $this->kernel = new DrupalKernel('unit_testing', drupal_classloader(), FALSE);
147 148
    $this->kernel->boot();

149 150 151 152 153 154 155
    // Create a minimal system.module configuration object so that the list of
    // enabled modules can be maintained allowing
    // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
    // Write directly to active storage to avoid early instantiation of
    // the event dispatcher which can prevent modules from registering events.
    \Drupal::service('config.storage')->write('system.module', array('enabled' => array()));

156 157 158 159 160
    // Collect and set a fixed module list.
    $class = get_class($this);
    $modules = array();
    while ($class) {
      if (property_exists($class, 'modules')) {
161 162 163 164 165
        // Only add the modules, if the $modules property was not inherited.
        $rp = new \ReflectionProperty($class, 'modules');
        if ($rp->class == $class) {
          $modules[$class] = $class::$modules;
        }
166 167 168
      }
      $class = get_parent_class($class);
    }
169 170 171
    // Modules have been collected in reverse class hierarchy order; modules
    // defined by base classes should be sorted first. Then, merge the results
    // together.
172 173 174 175 176
    if ($modules) {
      $modules = array_reverse($modules);
      $modules = call_user_func_array('array_merge_recursive', $modules);
      $this->enableModules($modules, FALSE);
    }
177
    // In order to use theme functions default theme config needs to exist.
178
    \Drupal::config('system.theme')->set('default', 'stark');
179 180 181 182 183

    // Tests based on this class are entitled to use Drupal's File and
    // StreamWrapper APIs.
    // @todo Move StreamWrapper management into DrupalKernel.
    // @see https://drupal.org/node/2028109
184
    $this->streamWrappers = array();
185 186 187 188 189 190
    // The public stream wrapper only depends on the file_public_path setting,
    // which is provided by UnitTestBase::setUp().
    $this->registerStreamWrapper('public', 'Drupal\Core\StreamWrapper\PublicStream');
    // The temporary stream wrapper is able to operate both with and without
    // configuration.
    $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream');
191 192 193
  }

  protected function tearDown() {
194 195 196
    if ($this->kernel instanceof DrupalKernel) {
      $this->kernel->shutdown();
    }
197 198 199 200 201 202
    // Before tearing down the test environment, ensure that no stream wrapper
    // of this test leaks into the parent environment. Unlike all other global
    // state variables in Drupal, stream wrappers are a global state construct
    // of PHP core, which has to be maintained manually.
    // @todo Move StreamWrapper management into DrupalKernel.
    // @see https://drupal.org/node/2028109
203 204
    foreach ($this->streamWrappers as $scheme => $type) {
      $this->unregisterStreamWrapper($scheme, $type);
205
    }
206
    parent::tearDown();
207 208
  }

209 210 211 212
  /**
   * Sets up the base service container for this test.
   *
   * Extend this method in your test to register additional service overrides
213 214
   * that need to persist a DrupalKernel reboot. This method is called whenever
   * the kernel is rebuilt.
215
   *
216 217 218
   * @see \DrupalUnitTestBase::setUp()
   * @see \DrupalUnitTestBase::enableModules()
   * @see \DrupalUnitTestBase::disableModules()
219
   */
220
  public function containerBuild(ContainerBuilder $container) {
221 222
    // Keep the container object around for tests.
    $this->container = $container;
223

224 225 226
    // Set the default language on the minimal container.
    $this->container->setParameter('language.default_values', Language::$defaultValues);

227
    $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
228
    $container->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory');
229

230
    $container
231 232
      ->register('config.storage', 'Drupal\Core\Config\FileStorage')
      ->addArgument($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]);
233

234
    $this->settingsSet('keyvalue_default', 'keyvalue.memory');
235
    $container->set('keyvalue.memory', $this->keyValueFactory);
236 237
    if (!$container->has('keyvalue')) {
      // TestBase::setUp puts a completely empty container in
238
      // $this->container which is somewhat the mirror of the empty
239 240 241 242 243
      // environment being set up. Unit tests need not to waste time with
      // getting a container set up for them. Drupal Unit Tests might just get
      // away with a simple container holding the absolute bare minimum. When
      // a kernel is overridden then there's no need to re-register the keyvalue
      // service but when a test is happy with the superminimal container put
244 245 246
      // together here, it still might a keyvalue storage for anything using
      // \Drupal::state() -- that's why a memory service was added in the first
      // place.
247 248 249 250
      $container->register('settings', 'Drupal\Component\Utility\Settings')
        ->setFactoryClass('Drupal\Component\Utility\Settings')
        ->setFactoryMethod('getSingleton');

251 252
      $container
        ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
253 254
        ->addArgument(new Reference('service_container'))
        ->addArgument(new Reference('settings'));
255

256 257
      $container->register('state', 'Drupal\Core\KeyValueStore\State')
        ->addArgument(new Reference('keyvalue'));
258
    }
259 260 261 262 263 264 265 266 267

    if ($container->hasDefinition('path_processor_alias')) {
      // Prevent the alias-based path processor, which requires a url_alias db
      // table, from being registered to the path processor manager. We do this
      // by removing the tags that the compiler pass looks for. This means the
      // url generator can safely be used within DUTB tests.
      $definition = $container->getDefinition('path_processor_alias');
      $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
    }
268 269 270 271 272

    if ($container->hasDefinition('password')) {
      $container->getDefinition('password')->setArguments(array(1));
    }

273 274
    $request = Request::create('/');
    $this->container->set('request', $request);
275 276
  }

277
  /**
278
   * Installs default configuration for a given list of modules.
279
   *
280 281 282 283 284 285 286 287 288 289
   * @param array $modules
   *   A list of modules for which to install default configuration.
   */
  protected function installConfig(array $modules) {
    foreach ($modules as $module) {
      if (!$this->container->get('module_handler')->moduleExists($module)) {
        throw new \RuntimeException(format_string("'@module' module is not enabled.", array(
          '@module' => $module,
        )));
      }
290
      \Drupal::service('config.installer')->installDefaultConfig('module', $module);
291 292 293 294 295 296 297 298
    }
    $this->pass(format_string('Installed default config: %modules.', array(
      '%modules' => implode(', ', $modules),
    )));
  }

  /**
   * Installs a specific table from a module schema definition.
299
   *
300 301
   * @param string $module
   *   The name of the module that defines the table's schema.
302 303
   * @param string|array $tables
   *   The name or an array of the names of the tables to install.
304
   */
305
  protected function installSchema($module, $tables) {
306 307 308 309 310
    // drupal_get_schema_unprocessed() is technically able to install a schema
    // of a non-enabled module, but its ability to load the module's .install
    // file depends on many other factors. To prevent differences in test
    // behavior and non-reproducible test failures, we only allow the schema of
    // explicitly loaded/enabled modules to be installed.
311
    if (!$this->container->get('module_handler')->moduleExists($module)) {
312 313 314 315
      throw new \RuntimeException(format_string("'@module' module is not enabled.", array(
        '@module' => $module,
      )));
    }
316 317 318 319 320 321 322 323 324 325
    $tables = (array) $tables;
    foreach ($tables as $table) {
      $schema = drupal_get_schema_unprocessed($module, $table);
      if (empty($schema)) {
        throw new \RuntimeException(format_string("Unknown '@table' table schema in '@module' module.", array(
          '@module' => $module,
          '@table' => $table,
        )));
      }
      $this->container->get('database')->schema()->createTable($table, $schema);
326 327 328 329 330
    }
    // We need to refresh the schema cache, as any call to drupal_get_schema()
    // would not know of/return the schema otherwise.
    // @todo Refactor Schema API to make this obsolete.
    drupal_get_schema(NULL, TRUE);
331 332 333 334
    $this->pass(format_string('Installed %module tables: %tables.', array(
      '%tables' => '{' . implode('}, {', $tables) . '}',
      '%module' => $module,
    )));
335 336 337 338 339 340
  }

  /**
   * Enables modules for this test.
   *
   * @param array $modules
341 342
   *   A list of modules to enable. Dependencies are not resolved; i.e.,
   *   multiple modules have to be specified with dependent modules first.
343
   *   The new modules are only added to the active module list and loaded.
344
   */
345 346 347
  protected function enableModules(array $modules) {
    // Set the list of modules in the extension handler.
    $module_handler = $this->container->get('module_handler');
348

349 350 351 352
    // Write directly to active storage to avoid early instantiation of
    // the event dispatcher which can prevent modules from registering events.
    $active_storage =  \Drupal::service('config.storage');
    $system_config = $active_storage->read('system.module');
353

354
    foreach ($modules as $module) {
355
      $module_handler->addModule($module, drupal_get_path('module', $module));
356 357
      // Maintain the list of enabled modules in configuration.
      $system_config['enabled'][$module] = 0;
358
    }
359
    $active_storage->write('system.module', $system_config);
360

361
    // Update the kernel to make their services available.
362
    $module_filenames = $module_handler->getModuleList();
363 364
    $this->kernel->updateModules($module_filenames, $module_filenames);

365
    // Ensure isLoaded() is TRUE in order to make _theme() work.
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    // Note that the kernel has rebuilt the container; this $module_handler is
    // no longer the $module_handler instance from above.
    $module_handler = $this->container->get('module_handler');
    $module_handler->reload();
    $this->pass(format_string('Enabled modules: %modules.', array(
      '%modules' => implode(', ', $modules),
    )));
  }

  /**
   * Disables modules for this test.
   *
   * @param array $modules
   *   A list of modules to disable. Dependencies are not resolved; i.e.,
   *   multiple modules have to be specified with dependent modules first.
   *   Code of previously active modules is still loaded. The modules are only
   *   removed from the active module list.
   */
  protected function disableModules(array $modules) {
    // Unset the list of modules in the extension handler.
    $module_handler = $this->container->get('module_handler');
    $module_filenames = $module_handler->getModuleList();
388
    $system_config = $this->container->get('config.factory')->get('system.module');
389 390
    foreach ($modules as $module) {
      unset($module_filenames[$module]);
391
      $system_config->clear('enabled.' . $module);
392
    }
393
    $system_config->save();
394 395 396 397 398
    $module_handler->setModuleList($module_filenames);
    $module_handler->resetImplementations();
    // Update the kernel to remove their services.
    $this->kernel->updateModules($module_filenames, $module_filenames);

399
    // Ensure isLoaded() is TRUE in order to make _theme() work.
400 401 402 403
    // Note that the kernel has rebuilt the container; this $module_handler is
    // no longer the $module_handler instance from above.
    $module_handler = $this->container->get('module_handler');
    $module_handler->reload();
404 405 406
    $this->pass(format_string('Disabled modules: %modules.', array(
      '%modules' => implode(', ', $modules),
    )));
407
  }
408

409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
  /**
   * Registers a stream wrapper for this test.
   *
   * @param string $scheme
   *   The scheme to register.
   * @param string $class
   *   The fully qualified class name to register.
   * @param int $type
   *   The Drupal Stream Wrapper API type. Defaults to
   *   STREAM_WRAPPERS_LOCAL_NORMAL.
   */
  protected function registerStreamWrapper($scheme, $class, $type = STREAM_WRAPPERS_LOCAL_NORMAL) {
    if (isset($this->streamWrappers[$scheme])) {
      $this->unregisterStreamWrapper($scheme);
    }
424
    $this->streamWrappers[$scheme] = $type;
425 426 427 428 429 430 431 432
    if (($type & STREAM_WRAPPERS_LOCAL) == STREAM_WRAPPERS_LOCAL) {
      stream_wrapper_register($scheme, $class);
    }
    else {
      stream_wrapper_register($scheme, $class, STREAM_IS_URL);
    }
    // @todo Revamp Drupal's stream wrapper API for D8.
    // @see https://drupal.org/node/2028109
433 434
    $wrappers = &drupal_static('file_get_stream_wrappers', array());
    $wrappers[STREAM_WRAPPERS_ALL][$scheme] = array(
435 436 437
      'type' => $type,
      'class' => $class,
    );
438 439 440
    if (($type & STREAM_WRAPPERS_WRITE_VISIBLE) == STREAM_WRAPPERS_WRITE_VISIBLE) {
      $wrappers[STREAM_WRAPPERS_WRITE_VISIBLE][$scheme] = $wrappers[STREAM_WRAPPERS_ALL][$scheme];
    }
441 442 443 444 445 446 447 448 449 450
  }

  /**
   * Unregisters a stream wrapper previously registered by this test.
   *
   * DrupalUnitTestBase::tearDown() automatically cleans up all registered
   * stream wrappers, so this usually does not have to be called manually.
   *
   * @param string $scheme
   *   The scheme to unregister.
451 452
   * @param int $type
   *   The Drupal Stream Wrapper API type of the scheme to unregister.
453
   */
454
  protected function unregisterStreamWrapper($scheme, $type) {
455 456 457 458
    stream_wrapper_unregister($scheme);
    unset($this->streamWrappers[$scheme]);
    // @todo Revamp Drupal's stream wrapper API for D8.
    // @see https://drupal.org/node/2028109
459 460 461 462 463 464
    $wrappers = &drupal_static('file_get_stream_wrappers', array());
    foreach ($wrappers as $filter => $schemes) {
      if (is_int($filter) && (($filter & $type) == $filter)) {
        unset($wrappers[$filter][$scheme]);
      }
    }
465 466
  }

467
}