DrupalUnitTestBase.php 16.6 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
  /**
   * A KeyValueMemoryFactory instance to use when building the container.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
   */
  protected $keyValueFactory;

67 68 69 70 71 72 73 74 75
  /**
   * A list of stream wrappers that have been registered for this test.
   *
   * @see \Drupal\simpletest\DrupalUnitTestBase::registerStreamWrapper()
   *
   * @var array
   */
  private $streamWrappers = array();

76 77 78 79 80 81 82 83
  /**
   * Overrides \Drupal\simpletest\UnitTestBase::__construct().
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

84
  /**
85
   * Overrides TestBase::beforePrepareEnvironment().
86
   */
87
  protected function beforePrepareEnvironment() {
88 89
    // Copy/prime extension file lists once to avoid filesystem scans.
    if (!isset($this->moduleFiles)) {
90 91 92
      $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();
93
    }
94
  }
95

96 97 98 99
  /**
   * Sets up Drupal unit test environment.
   */
  protected function setUp() {
100 101
    $this->keyValueFactory = new KeyValueMemoryFactory();

102
    parent::setUp();
103

104
    // Build a minimal, partially mocked environment for unit tests.
105
    $this->containerBuild(\Drupal::getContainer());
106
    // Make sure it survives kernel rebuilds.
107
    $GLOBALS['conf']['container_service_providers']['TestServiceProvider'] = 'Drupal\simpletest\TestServiceProvider';
108

109 110 111
    \Drupal::state()->set('system.module.files', $this->moduleFiles);
    \Drupal::state()->set('system.theme.files', $this->themeFiles);
    \Drupal::state()->set('system.theme.data', $this->themeData);
112

113
    // Bootstrap the kernel.
114
    // No need to dump it; this test runs in-memory.
115
    $this->kernel = new DrupalKernel('unit_testing', drupal_classloader(), FALSE);
116 117
    $this->kernel->boot();

118 119 120 121 122 123 124
    // 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()));

125 126 127 128 129
    // Collect and set a fixed module list.
    $class = get_class($this);
    $modules = array();
    while ($class) {
      if (property_exists($class, 'modules')) {
130 131 132 133 134
        // Only add the modules, if the $modules property was not inherited.
        $rp = new \ReflectionProperty($class, 'modules');
        if ($rp->class == $class) {
          $modules[$class] = $class::$modules;
        }
135 136 137
      }
      $class = get_parent_class($class);
    }
138 139 140 141 142 143
    // Modules have been collected in reverse class hierarchy order; modules
    // defined by base classes should be sorted first. Then, merge the results
    // together.
    $modules = array_reverse($modules);
    $modules = call_user_func_array('array_merge_recursive', $modules);
    $this->enableModules($modules, FALSE);
144
    // In order to use theme functions default theme config needs to exist.
145
    \Drupal::config('system.theme')->set('default', 'stark');
146 147 148 149 150 151 152 153 154 155 156

    // 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
    // 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');
157 158 159
  }

  protected function tearDown() {
160
    $this->kernel->shutdown();
161 162 163 164 165 166 167 168 169
    // 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
    foreach ($this->streamWrappers as $scheme) {
      $this->unregisterStreamWrapper($scheme);
    }
170
    parent::tearDown();
171 172
  }

173 174 175 176
  /**
   * Sets up the base service container for this test.
   *
   * Extend this method in your test to register additional service overrides
177 178
   * that need to persist a DrupalKernel reboot. This method is called whenever
   * the kernel is rebuilt.
179
   *
180 181 182
   * @see \DrupalUnitTestBase::setUp()
   * @see \DrupalUnitTestBase::enableModules()
   * @see \DrupalUnitTestBase::disableModules()
183
   */
184
  public function containerBuild(ContainerBuilder $container) {
185 186
    // Keep the container object around for tests.
    $this->container = $container;
187

188 189 190
    // Set the default language on the minimal container.
    $this->container->setParameter('language.default_values', Language::$defaultValues);

191
    $container->register('lock', 'Drupal\Core\Lock\NullLockBackend');
192
    $this->settingsSet('cache', array('default' => 'cache.backend.memory'));
193

194
    $container
195 196
      ->register('config.storage', 'Drupal\Core\Config\FileStorage')
      ->addArgument($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]);
197

198
    $this->settingsSet('keyvalue_default', 'keyvalue.memory');
199
    $container->set('keyvalue.memory', $this->keyValueFactory);
200 201
    if (!$container->has('keyvalue')) {
      // TestBase::setUp puts a completely empty container in
202
      // $this->container which is somewhat the mirror of the empty
203 204 205 206 207
      // 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
208 209 210
      // 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.
211 212 213 214
      $container->register('settings', 'Drupal\Component\Utility\Settings')
        ->setFactoryClass('Drupal\Component\Utility\Settings')
        ->setFactoryMethod('getSingleton');

215 216
      $container
        ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
217 218
        ->addArgument(new Reference('service_container'))
        ->addArgument(new Reference('settings'));
219

220 221
      $container->register('state', 'Drupal\Core\KeyValueStore\State')
        ->addArgument(new Reference('keyvalue'));
222
    }
223 224 225 226 227 228 229 230 231

    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');
    }
232 233 234 235 236

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

237 238
    $request = Request::create('/');
    $this->container->set('request', $request);
239 240
  }

241
  /**
242
   * Installs default configuration for a given list of modules.
243
   *
244 245 246 247 248 249 250 251 252 253
   * @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,
        )));
      }
254
      \Drupal::service('config.installer')->installDefaultConfig('module', $module);
255 256 257 258 259 260 261 262
    }
    $this->pass(format_string('Installed default config: %modules.', array(
      '%modules' => implode(', ', $modules),
    )));
  }

  /**
   * Installs a specific table from a module schema definition.
263
   *
264 265
   * @param string $module
   *   The name of the module that defines the table's schema.
266 267
   * @param string|array $tables
   *   The name or an array of the names of the tables to install.
268
   */
269
  protected function installSchema($module, $tables) {
270 271 272 273 274
    // 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.
275
    if (!$this->container->get('module_handler')->moduleExists($module)) {
276 277 278 279
      throw new \RuntimeException(format_string("'@module' module is not enabled.", array(
        '@module' => $module,
      )));
    }
280 281 282 283 284 285 286 287 288 289
    $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);
290 291 292 293 294
    }
    // 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);
295 296 297 298
    $this->pass(format_string('Installed %module tables: %tables.', array(
      '%tables' => '{' . implode('}, {', $tables) . '}',
      '%module' => $module,
    )));
299 300 301 302 303 304
  }

  /**
   * Enables modules for this test.
   *
   * @param array $modules
305 306
   *   A list of modules to enable. Dependencies are not resolved; i.e.,
   *   multiple modules have to be specified with dependent modules first.
307
   *   The new modules are only added to the active module list and loaded.
308
   */
309 310 311 312
  protected function enableModules(array $modules) {
    // Set the list of modules in the extension handler.
    $module_handler = $this->container->get('module_handler');
    $module_filenames = $module_handler->getModuleList();
313 314 315 316
    // 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');
317 318
    foreach ($modules as $module) {
      $module_filenames[$module] = drupal_get_filename('module', $module);
319 320
      // Maintain the list of enabled modules in configuration.
      $system_config['enabled'][$module] = 0;
321
    }
322
    $active_storage->write('system.module', $system_config);
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
    $module_handler->setModuleList($module_filenames);
    $module_handler->resetImplementations();
    // Update the kernel to make their services available.
    $this->kernel->updateModules($module_filenames, $module_filenames);

    // Ensure isLoaded() is TRUE in order to make theme() work.
    // 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();
351
    $system_config = $this->container->get('config.factory')->get('system.module');
352 353
    foreach ($modules as $module) {
      unset($module_filenames[$module]);
354
      $system_config->clear('enabled.' . $module);
355
    }
356
    $system_config->save();
357 358 359 360 361 362
    $module_handler->setModuleList($module_filenames);
    $module_handler->resetImplementations();
    // Update the kernel to remove their services.
    $this->kernel->updateModules($module_filenames, $module_filenames);

    // Ensure isLoaded() is TRUE in order to make theme() work.
363 364 365 366
    // 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();
367 368 369
    $this->pass(format_string('Disabled modules: %modules.', array(
      '%modules' => implode(', ', $modules),
    )));
370
  }
371

372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
  /**
   * 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);
    }
    $this->streamWrappers[$scheme] = $scheme;
    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
    $wrappers = &drupal_static('file_get_stream_wrappers');
    $wrappers[$scheme] = array(
      'type' => $type,
      'class' => $class,
    );
    $wrappers[STREAM_WRAPPERS_ALL] = $wrappers;
  }

  /**
   * 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.
   */
  protected function unregisterStreamWrapper($scheme) {
    stream_wrapper_unregister($scheme);
    unset($this->streamWrappers[$scheme]);
    // @todo Revamp Drupal's stream wrapper API for D8.
    // @see https://drupal.org/node/2028109
    $wrappers = &drupal_static('file_get_stream_wrappers');
    unset($wrappers[$scheme]);
    unset($wrappers[STREAM_WRAPPERS_ALL][$scheme]);
  }

423
}