Drupal 10测试迁移过程插件:自定义插件和单元内核测试指南

Drupal 10:测试迁移过程插件

注意:这篇文章发布已超过两年,因此其中包含的信息可能已过时。如果您发现有误,请留下评论,我公司会尽力更正。

2024年6月9日 - 阅读时长30分钟

Drupal的迁移系统允许使用多种不同的插件来执行源、处理和目标功能。在Drupal开发中,合理利用这些插件能极大提升开发效率,当然,在Drupal升级过程中,迁移插件的稳定性也至关重要,尤其是到Drupal11版本,可能会有更多新的要求和变化。

处理插件负责将数据复制到目标位置,有时还会对数据进行处理。有许多不同的处理插件,可让您以不同的方式获取数据并将其应用到目标字段。

核心的Migrate模块和优秀的Migrate Plus模块都包含许多不同的处理插件,您可以使用这些插件以不同的方式处理数据。这也体现了Drupal模块开发的丰富性和灵活性。

开箱即用的默认处理插件是get插件,您可以在迁移脚本中像这样使用它。

destination_field:
  plugin: get
  source: source_field

通常会将其简写为以下形式,功能完全相同。

destination_field: source_field

大多数时候,您会希望避免创建自定义插件,但有时迁移需求会使您不得不使用它们。您可能会发现源数据非常杂乱,在将其导入网站之前需要清理。处理插件是实现这一目的的好方法,但至关重要的是,您要编写测试来覆盖可能遇到的各种情况。

在本文中,我们将探讨两个以不同方式构建的自定义迁移处理插件,以及如何对它们进行测试。这将深入涉及Drupal插件管理、依赖注入、使用PHPUnit进行单元测试和数据提供程序等相关概念。

首先,让我们看看本文将使用的迁移脚本。此迁移示例的所有源代码都可以在相关代码库中找到。

我们将使用embedded_data迁移源插件,以便可以直接将源数据嵌入到迁移脚本中。这个插件对于快速迁移很有用,而且在这里对我们也有帮助,因为我们可以把它用作教学工具。这里的源数据故意设置得很杂乱,以模拟杂乱的迁移源。

id: migration_process_test
label: Testing custom migration process plugins

source:
  plugin: embedded_data
  data_rows:
    - data_id: 1
      data_title: '<p>About us page</p>'
      data_content: '<p>The about us page content.<span></span></p>'
    - data_id: 2
      data_title: '<p>contact Us page</p>'
      data_content: '<p>Contact us via the address example@example.com.</p>'
  ids:
    data_id:
      type: integer

process:
  # Title field.
  title:
    - plugin: reformat_title
      source: data_title
  # Body field.
  body/0/value:
    - plugin: fix_data_content
      source: data_content
  body/0/format:
    plugin: default_value
    default_value: "basic_html"

destination:
  plugin: entity:node
  default_bundle: page

我们在上述代码中使用了两个自定义处理插件。

  • reformat_title - 我们从源获取的数据中,标题包含标记,我们希望去除这些标记。此外,源数据中的许多页面标题都包含“page”这个词,因此我们希望在出现该词的地方将其删除。最后一步,标题中的每个单词首字母应大写。

    这个迁移插件没有依赖项,因此将使用单元测试设置进行测试。

  • fix_data_content - 与许多迁移源一样,正文字段的源数据非常杂乱。有一些不包含文本的标签,我们希望在将标记添加到网站之前将其从标记中删除。标记中还添加了一些硬编码的联系电子邮件地址,我们希望将这些地址替换为新网站的电子邮件地址。

    这个迁移插件需要将网站配置工厂(config.factory)作为服务注入,以便我们可以获取网站的电子邮件地址。由于这个插件的设置更复杂,我们将使用内核测试来测试此插件。

为了尽可能简化,我们没有为这些处理插件使用任何配置参数。

此迁移的目标是页面内容类型,这是我们使用标准Drupal安装配置文件时会得到的类型。

一、创建重新格式化标题迁移插件

在迁移中创建处理插件相当容易(谢天谢地)。Drupal插件管理系统使用注解来识别插件名称,并生成我们可以使用的插件对象。

在迁移脚本中,我们将重新格式化标题插件的ID定义为“reformat_title”,因此我们需要在src/Plugin/migrate/process目录下创建一个继承自\Drupal\migrate\ProcessPluginBase的类。

我们在这个类中添加以下注解,以表明它是一个处理插件。所有处理插件类都必须包含此注解。

@MigrateProcessPlugin(id = "reformat_title")

处理插件类的transform()方法是迁移系统在迁移期间将执行的方法,它需要迁移系统的两个对象。这两个对象分别是当前迁移系统可执行对象和表示当前正在处理的行的对象。

reformat_title插件的整个插件类并不大,它只包含注解和transform方法。

<?php

namespace Drupal\migration_process_test\Plugin\migrate\process;

use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;

/**
 * Reformat the title of the page.
 *
 * @code
 * title:
 *   plugin: reformat_title
 *   source: body/0/value
 * @endcode
 *
 * @MigrateProcessPlugin(id = "reformat_title")
 */
class ReformatTitle extends ProcessPluginBase {

  /**
   * {@inheritDoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    if ($value === NULL) {
      return $value;
    }

    // Strip any markup that the title might have.
    $value = strip_tags($value);

    // Strip any ending "page" words.
    $value = preg_replace('/\spage\s?$/', '', $value);

    // Make the string sentence case.
    $value = ucwords($value);

    return $value;
  }

}

这里的transform方法只是对来自源的字符串执行我们想要的更改。

二、对重新格式化标题迁移插件进行单元测试

reformat_title迁移处理插件不需要任何额外的依赖项,因此可以使用单元测试进行测试。

为了对迁移处理插件进行单元测试,我们需要继承\Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase类。这个类继承自Drupal核心的\Drupal\Tests\UnitTestCase类,并添加了一些样板代码,这样我们就可以实例化插件对象,而无需编写自己的模拟代码。

正如我前面提到的,处理插件的transform()方法是迁移系统在迁移期间将执行的方法,它需要迁移系统的两个对象。这两个对象分别是当前迁移系统可执行对象和表示当前正在处理的行的对象。MigrateProcessTestCase对象将创建两个属性,我们可以使用这些属性来调用transform()方法,而不必担心从哪里获取这些对象。

如果您的处理插件需要从行对象中提取一些值,您可以像平常一样使用全局属性来模拟方法并返回值。

要将处理插件创建为可用对象,我们只需调用它即可。构造函数(在\Drupal\Component\Plugin\PluginBase中定义)需要一些简单的参数来实例化对象,但由于这些参数都不是对象,我们不需要进行任何模拟。

$plugin = new ReformatTitle([], 'reformat_title', []);

有了插件对象后,我们就可以调用transform()方法,传入要测试的源值以及父类中生成的迁移可执行对象和行对象参数。这意味着我们的测试可以简化为以下两行代码。

$value = $plugin->transform('<p>About us page</p>', $this->migrateExecutable, $this->row, 'title');
$this->assertEquals('About Us', $value);

对于单个值来说,这样做没问题,但运行处理插件测试的更好方法是使用数据提供程序。

数据提供程序是PHPUnit的插件,它会多次调用我们的测试用例,每次都使用不同的测试设置。要设置它们,您只需在测试文档块注释中添加@dataProvider属性,并定义将用于数据提供程序的方法。

以下是reformat_title迁移处理插件的完整单元测试类,它位于我们迁移模块的test/Unit/Plugin/migrate/process目录中。

<?php

namespace Drupal\Tests\migration_process_test\Unit\Plugin\migrate\process;

use Drupal\migration_process_test\Plugin\migrate\process\ReformatTitle;
use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase;

/**
 * Tests the reformat_title migration plugin.
 */
class ReformatTitleTest extends MigrateProcessTestCase {

  /**
   * Test that different title values reformat correctly.
   *
   * @dataProvider titleIsReformattedDataProvider
   */
  public function testTitleIsReformatted($sourceValue, $expectedResult) {
    $plugin = new ReformatTitle([], 'reformat_title', []);
    $value = $plugin->transform($sourceValue, $this->migrateExecutable, $this->row, 'title');
    $this->assertEquals($expectedResult, $value);
  }

  /**
   * Data provider for testTitleIsReformatted.
   *
   * @return array
   *   The data to be tested.
   */
  public function titleIsReformattedDataProvider() {
    return [
      [
        '<p>About us page</p>',
        'About Us',
      ],
      [
        '<p>contact Us page</p>',
        'Contact Us',
      ],
    ];
  }

}

现在,我们可以使用大量不同的数据组合来测试我们的处理插件。

三、创建修复数据内容迁移插件

fix_data_content迁移处理插件的设置与reformat_title插件相同,但有一个例外。我们希望向这个插件注入一个依赖项,因此可以使用Drupal服务。

顺便提一下,我实际上很难找到一个足够简单的示例来演示所涉及的概念,但又不会复杂到需要一堆其他服务和依赖项才能运行。为此,我决定最简单的做法是进行简单的电子邮件地址替换。我们只需要从Drupal配置中获取网站电子邮件地址,这意味着要将config.factory服务注入到插件中。

plugin.manager.migrate.process服务知道我们可能希望将依赖项注入到对象中,因此会查看插件类是否实现了\Drupal\Core\Plugin\ContainerFactoryPluginInterface接口。如果实现了该接口,插件管理器将调用类中的静态create()方法来创建对象并注入任何依赖项。

对于这个插件,我们希望访问一组配置,因此获取config.factory服务并提取system.site配置,然后将其存储在一个变量中。

fix_data_content迁移处理插件的完整类并不太大。实际上,它主要由设置对象所需的依赖项的样板代码组成。

<?php

namespace Drupal\migration_process_test\Plugin\migrate\process;

use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Fix any broken markup in the source field.
 *
 * @code
 * body/0/value:
 *   plugin: fix_data_content
 * @endcode
 *
 * @MigrateProcessPlugin(id = "fix_data_content")
 */
class FixDataContent extends ProcessPluginBase implements ContainerFactoryPluginInterface {

  /**
   * The config factory object.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected ImmutableConfig $siteConfig;

  /**
   * {@inheritDoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = new self($configuration, $plugin_id, $plugin_definition);

    $instance->siteConfig = $container->get('config.factory')->get('system.site');

    return $instance;
  }

  /**
   * {@inheritDoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    if ($value === NULL) {
      return $value;
    }

    // Strip any empty elements.
    $value = preg_replace('/<span>\s*<\/span>/', '', $value);
    $value = preg_replace('/<p>\s*<\/p>/', '', $value);

    // Replace all instance of example@example.com with our site email.
    $value = preg_replace('/example@example.com/', $this->siteConfig->get('mail'), $value);

    return $value;
  }

}

这里的transform方法将使用几个正则表达式来更改传递给正文字段的数据。

四、对修复数据内容迁移插件进行内核测试

内核测试与单元测试略有不同,它允许您对Drupal进行最小化引导。在测试设置期间,将设置一个最小化安装的Drupal网站,然后您可以使用所需的模块、实体和模式对其进行扩展。这包括安装您当前正在测试的模块。

例如,如果您想对用户实体进行测试,那么您需要安装用户模块,这是合理的。但您还需要安装用户实体模式,以便生成实体存在所需的表。

对于我们的fix_data_content迁移处理插件,由于它使用了Drupal配置,因此在实际运行任何测试之前需要进行更多的设置管理。由于Drupal是最小化引导的,我们可以访问系统中的一些核心服务,其中之一就是配置系统。

Drupal中的内核测试继承自\Drupal\KernelTests\KernelTestBase类。运行时,这个类会自动检测一个名为$modules的类属性,它将使用该属性来确定必须安装的模块列表。在我们的例子中,我们只需要核心的migrate模块、我们自己的自定义模块(名为migration_process_test)以及用于核心系统和配置设置的system模块。

protected static $modules = [
  'migrate',
  'migration_process_test',
  'system',
];

通常,在测试的setUp()方法中设置测试条件是个好主意,这是PHPUnit测试框架提供的一部分。这个函数会自动调用,因此为我们提供了一种在运行测试之前设置Drupal以满足我们所有需求的方法。确保首先调用父类的setUp()方法。

protected function setUp(): void {
  parent::setUp();
}

我们内核测试类中的setUp()方法需要执行几个操作。

首先,我们需要设置配置,以便system.site.mail配置设置包含一个已知值。为此,我们只需向Drupal请求config.factory服务,并更改我们将在测试中使用的值。

// Update the site configuration with our test email address.
\Drupal::service('config.factory')->getEditable('system.site');
$system = $this->config('system.site');
$system
  ->set('mail', 'test@example2.com')
  ->save();

接下来,由于KernelTestBase类没有继承迁移的MigrateProcessTestCase类,我们没有在单元测试用例中免费获得的迁移可执行对象和行对象。在这种情况下,我们需要在setUp方法中添加一点代码来生成这些对象作为模拟对象。

// Create the test row and executable objects.
$this->row = $this->getMockBuilder('Drupal\migrate\Row')
  ->disableOriginalConstructor()
  ->getMock();
$this->migrateExecutable = $this->getMockBuilder('Drupal\migrate\MigrateExecutable')
  ->disableOriginalConstructor()
  ->getMock();

这是setUp()方法中所需的所有内容,现在我们可以在测试用例中生成我们的插件对象了。

为了在测试用例中生成迁移处理插件,我们需要使用plugin.manager.migrate.process服务。这个插件的createInstance()方法会自动调用我们插件类中的create()方法,我们使用该方法来生成插件正常工作所需的依赖项。为了调用createInstance()方法,我们需要提供几个额外的属性。这些属性包括一个迁移对象和我们想要传递给插件本身的任何配置。

plugin.manager.migration服务有一个名为createStubMigration()的方法,我们可以在这里使用它来生成一个简单的测试迁移。这个迁移实际上不会在这里使用(这是一个处理插件),所以它只需要进行最低限度的设置。

// Create migration stub.
$migration = \Drupal::service('plugin.manager.migration')
  ->createStubMigration([
    'id' => 'test',
    'source' => [],
    'process' => [],
    'destination' => [
      'plugin' => 'entity:node',
  ],
]);

// Set plugin configuration.
$configuration = [];

// Generate the plugin via the plugin.manager.migrate.process service.
$plugin = \Drupal::service('plugin.manager.migrate.process')
      ->createInstance('fix_data_content', $configuration, $migration);

现在,$plugin变量包含我们插件类的一个完全可用的实例,我们可以像之前一样调用它的transform()方法。

$value = $plugin->transform('<p>Some text.<span></span></p>', $this->migrateExecutable, $this->row, 'field_body');
$this->assertEquals('<p>Some text.</p>', $value);

当然,这只是一个单一的测试用例,应该像单元测试示例一样将其抽象为数据提供程序测试设置。

在我们的自定义迁移模块的test/Kernel/Plugin/migrate/process目录中。

五、在内核测试中模拟服务

我们上面还没有涉及到的另一个考虑因素是在内核测试中模拟服务的能力。在测试插件时,这很有用,因为您可以模拟对数据库的任何调用,以便向单元测试返回已知数据。

一个常见的例子是模拟migrate.lookup服务,该服务用于从源数据查找映射到目标数据的值。要模拟这个类,我们可以做如下操作。

$migratePluginManager = $this->getMockBuilder('\Drupal\migrate\Plugin\MigrationPluginManager')
  ->disableOriginalConstructor()
  ->getMock();
$this->migrateLookup = $this->getMockBuilder('\Drupal\migrate\MigrateLookup')
  ->setConstructorArgs([$migratePluginManager])
  ->getMock();

然后,我们需要将这个模拟版本的migrate.lookup服务注入到Drupal容器系统中。我们通过获取容器,将migrate.lookup设置为我们的自定义插件,然后将容器对象设置回Drupal中。

$container = \Drupal::getContainer();
$container->set('migrate.lookup', $this->migrateLookup);
\Drupal::setContainer($container);

这样做之后,我们的模拟migrate.lookup服务将被使用,这意味着我们可以在测试代码中做如下操作。

$this->migrateLookup->method('lookup')->willReturn([['mid' => 1]]);

这样,我们不需要确保存在用于模拟数据的数据库,模拟的migrate.lookup可以返回我们需要的任何值。

六、结论

一旦您习惯为迁移插件设置所需的单元和内核测试类,就可以考虑使用测试驱动开发来创建它们。使用测试驱动开发有助于编写所需的代码,而无需实际运行迁移。在Drupal模块开发以及Drupal升级到如Drupal11版本的过程中,这种测试驱动开发的方式能更好地保证插件的稳定性和兼容性。

在编写复杂迁移时,测试迁移处理插件是一个非常强大的工具。它不仅可以帮助您发现并纠正源数据中的边缘情况,还可以加快迁移开发速度。

我公司参与过处理大量数据的网站迁移;通常涉及大量的依赖项来查找字段值。这可能意味着为了测试迁移,您需要花费数小时进行设置,才能看到迁移的实际效果。通过为处理插件使用单元测试,您可以快速更新数据提供程序中的边缘情况,在插件中解决这些问题,并提交更改。

如果您想要此模块的完整源代码,可在相关代码库中找到。在设置自定义迁移插件时,您可以随意将其用作骨架。

如果您对迁移插件有任何评论或问题,请在下面的评论中告诉我公司。或者,通过联系表单与我公司联系,以便进行更深入的探讨。