DrupalUnitTestBase.php 11.9 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php

/**
 * @file
 * Contains Drupal\simpletest\DrupalUnitTestBase.
 */

namespace Drupal\simpletest;

10
use Drupal\Core\DependencyInjection\ContainerBuilder;
11
use Drupal\Core\DrupalKernel;
12
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
13
use Symfony\Component\DependencyInjection\Reference;
14
use Drupal\Core\Database\Database;
15
use Symfony\Component\DependencyInjection\ContainerInterface;
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

/**
 * 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.
 *
 * @see DrupalUnitTestBase::$modules
 * @see DrupalUnitTestBase::enableModules()
 */
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.
   *
   * @see DrupalUnitTestBase::enableModules()
   * @see DrupalUnitTestBase::setUp()
   *
   * @var array
   */
  public static $modules = array();

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

58 59 60 61 62 63 64
  /**
   * A KeyValueMemoryFactory instance to use when building the container.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
   */
  protected $keyValueFactory;

65 66 67 68 69 70 71 72
  /**
   * Overrides \Drupal\simpletest\UnitTestBase::__construct().
   */
  function __construct($test_id = NULL) {
    parent::__construct($test_id);
    $this->skipClasses[__CLASS__] = TRUE;
  }

73 74 75 76 77 78 79 80 81
  /**
   * Sets up Drupal unit test environment.
   *
   * @see DrupalUnitTestBase::$modules
   * @see DrupalUnitTestBase
   */
  protected function setUp() {
    // Copy/prime extension file lists once to avoid filesystem scans.
    if (!isset($this->moduleFiles)) {
82 83 84
      $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();
85 86
    }

87 88
    $this->keyValueFactory = new KeyValueMemoryFactory();

89
    parent::setUp();
90
    // Build a minimal, partially mocked environment for unit tests.
91 92 93
    $this->containerBuild(drupal_container());
    // Make sure it survives kernel rebuilds.
    $GLOBALS['conf']['container_bundles'][] = 'Drupal\simpletest\TestBundle';
94

95 96 97
    \Drupal::state()->set('system.module.files', $this->moduleFiles);
    \Drupal::state()->set('system.theme.files', $this->themeFiles);
    \Drupal::state()->set('system.theme.data', $this->themeData);
98

99
    // Bootstrap the kernel.
100
    // No need to dump it; this test runs in-memory.
101
    $this->kernel = new DrupalKernel('unit_testing', TRUE, drupal_classloader(), FALSE);
102 103
    $this->kernel->boot();

104 105 106 107 108
    // Collect and set a fixed module list.
    $class = get_class($this);
    $modules = array();
    while ($class) {
      if (property_exists($class, 'modules')) {
109 110 111 112 113
        // Only add the modules, if the $modules property was not inherited.
        $rp = new \ReflectionProperty($class, 'modules');
        if ($rp->class == $class) {
          $modules[$class] = $class::$modules;
        }
114 115 116
      }
      $class = get_parent_class($class);
    }
117 118 119 120 121 122
    // 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);
123 124
    // In order to use theme functions default theme config needs to exist.
    config('system.theme')->set('default', 'stark');
125 126 127 128 129
  }

  protected function tearDown() {
    $this->kernel->shutdown();
    parent::tearDown();
130 131
  }

132 133 134 135
  /**
   * Sets up the base service container for this test.
   *
   * Extend this method in your test to register additional service overrides
136 137
   * that need to persist a DrupalKernel reboot. This method is called whenever
   * the kernel is rebuilt.
138 139 140
   *
   * @see DrupalUnitTestBase::setUp()
   * @see DrupalUnitTestBase::enableModules()
141
   * @see DrupalUnitTestBase::disableModules()
142
   */
143
  public function containerBuild(ContainerBuilder $container) {
144
    global $conf;
145 146
    // Keep the container object around for tests.
    $this->container = $container;
147

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

151
    $container
152 153
      ->register('config.storage', 'Drupal\Core\Config\FileStorage')
      ->addArgument($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]);
154

155
    $conf['keyvalue_default'] = 'keyvalue.memory';
156
    $container->set('keyvalue.memory', $this->keyValueFactory);
157 158 159 160 161 162 163 164
    if (!$container->has('keyvalue')) {
      // TestBase::setUp puts a completely empty container in
      // drupal_container() which is somewhat the mirror of the empty
      // 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
165
      // together here, it still might a keyvalue storage for anything (for
166 167
      // eg. module_enable) using \Drupal::state() -- that's why a memory
      // service was added in the first place.
168 169 170
      $container
        ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory')
        ->addArgument(new Reference('service_container'));
171 172 173 174 175

      $container->register('state', 'Drupal\Core\KeyValueStore\KeyValueStoreInterface')
        ->setFactoryService(new Reference('keyvalue'))
        ->setFactoryMethod('get')
        ->addArgument('state');
176
    }
177 178 179 180 181 182 183 184 185 186

    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');
    }

187 188
  }

189
  /**
190
   * Installs default configuration for a given list of modules.
191
   *
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
   * @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,
        )));
      }
      config_install_default_config('module', $module);
    }
    $this->pass(format_string('Installed default config: %modules.', array(
      '%modules' => implode(', ', $modules),
    )));
  }

  /**
   * Installs a specific table from a module schema definition.
211
   *
212 213
   * @param string $module
   *   The name of the module that defines the table's schema.
214 215
   * @param string|array $tables
   *   The name or an array of the names of the tables to install.
216
   */
217
  protected function installSchema($module, $tables) {
218 219 220 221 222
    // 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.
223
    if (!$this->container->get('module_handler')->moduleExists($module)) {
224 225 226 227
      throw new \RuntimeException(format_string("'@module' module is not enabled.", array(
        '@module' => $module,
      )));
    }
228 229 230 231 232 233 234 235 236 237
    $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);
238 239 240 241 242
    }
    // 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);
243 244 245 246
    $this->pass(format_string('Installed %module tables: %tables.', array(
      '%tables' => '{' . implode('}, {', $tables) . '}',
      '%module' => $module,
    )));
247 248 249 250 251 252
  }

  /**
   * Enables modules for this test.
   *
   * @param array $modules
253 254
   *   A list of modules to enable. Dependencies are not resolved; i.e.,
   *   multiple modules have to be specified with dependent modules first.
255
   *   The new modules are only added to the active module list and loaded.
256
   */
257 258 259 260 261 262
  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();
    foreach ($modules as $module) {
      $module_filenames[$module] = drupal_get_filename('module', $module);
263
    }
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    $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();
    foreach ($modules as $module) {
      unset($module_filenames[$module]);
294
    }
295 296 297 298 299 300
    $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.
301 302 303 304
    // 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();
305 306 307
    $this->pass(format_string('Disabled modules: %modules.', array(
      '%modules' => implode(', ', $modules),
    )));
308
  }
309

310
}