diff --git a/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml b/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml
new file mode 100644
index 0000000000000000000000000000000000000000..44008ed49e64972e7fdb2fd661e994cd26ae6523
--- /dev/null
+++ b/tests/modules/ui_patterns_test/components/test-component/test-component.component.yml
@@ -0,0 +1,11 @@
+name: "UI Patterns Test component"
+props:
+  type: object
+  properties:
+    string:
+      title: "String"
+      type: "string"
+slots:
+  slot:
+    title: "Slot"
+    description: "Slot."
diff --git a/tests/modules/ui_patterns_test/components/test-component/test-component.twig b/tests/modules/ui_patterns_test/components/test-component/test-component.twig
new file mode 100644
index 0000000000000000000000000000000000000000..4414413afb6b20bdaf3209e2afb6ac4956fbd308
--- /dev/null
+++ b/tests/modules/ui_patterns_test/components/test-component/test-component.twig
@@ -0,0 +1,8 @@
+<div id="ui-patterns-test-component">
+  <div id="ui-patterns-props-string">
+    {{ string }}
+  </div>
+  <div id="ui-patterns-slots-slot">
+    {{ slot }}
+  </div>
+</div>
diff --git a/tests/modules/ui_patterns_test/ui_patterns_test.tokens.inc b/tests/modules/ui_patterns_test/ui_patterns_test.tokens.inc
new file mode 100644
index 0000000000000000000000000000000000000000..6b1ae55cf7285acf441d3c22f1c8c6b997ed25eb
--- /dev/null
+++ b/tests/modules/ui_patterns_test/ui_patterns_test.tokens.inc
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Builds placeholder replacement tokens for entity-related data.
+ */
+
+use Drupal\Core\Render\BubbleableMetadata;
+
+/**
+ * Implements hook_token_info().
+ */
+function ui_patterns_test_token_info(): array {
+  $type = [
+    'name' => t('Node Tests'),
+    'description' => t('Tokens related to individual content items, or "nodes".'),
+    'needs-data' => 'entity_test',
+  ];
+
+  // Core tokens for nodes.
+  $entity['id'] = [
+    'name' => t("Entity ID"),
+    'description' => t('The unique ID of the content item, or "entity_test".'),
+  ];
+
+  return [
+    'types' => ['node' => $type],
+    'tokens' => ['node' => $entity],
+  ];
+}
+
+/**
+ * Implements hook_tokens().
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+function ui_patterns_test_tokens(string $type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata): array {
+  $replacements = [];
+
+  if ($type == 'node' && !empty($data['node'])) {
+    $entity = $data['node'];
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        // Simple key values on the node.
+        case 'id':
+          $replacements[$original] = $entity->id();
+          break;
+
+        case 'name':
+          $replacements[$original] = $entity->name();
+          break;
+
+      }
+    }
+  }
+  return $replacements;
+}
diff --git a/tests/src/Kernel/Source/FieldPropertySourceTest.php b/tests/src/Kernel/Source/FieldPropertySourceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a1046b1ab0f96efd4e26a729c965bbf8f6784d25
--- /dev/null
+++ b/tests/src/Kernel/Source/FieldPropertySourceTest.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\ui_patterns\Kernel\Source;
+
+use Drupal\Tests\ui_patterns\Kernel\SourcePluginsTestBase;
+
+/**
+ * Test FieldPropertySource.
+ *
+ * @coversDefaultClass \Drupal\ui_patterns\Plugin\UiPatterns\Source\FieldPropertySource
+ * @group ui_patterns
+ */
+class FieldPropertySourceTest extends SourcePluginsTestBase {
+
+  /**
+   * Test Field Property Plugin.
+   */
+  public function testPlugin(): void {
+    $testData = self::loadTestDataFixture(__DIR__ . "/../../fixtures/TestDataSet.yml");
+    $testSets = $testData->getTestSets();
+    foreach ($testSets as $test_set_name => $test_set) {
+      if (!str_starts_with($test_set_name, 'field_property_')) {
+        continue;
+      }
+      $this->runSourcePluginTest($test_set);
+    }
+  }
+
+}
diff --git a/tests/src/Kernel/Source/TextWidgetTest.php b/tests/src/Kernel/Source/TextWidgetTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3f8c294617277aa117ba0c57b1a9613f6dc11e7e
--- /dev/null
+++ b/tests/src/Kernel/Source/TextWidgetTest.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\ui_patterns\Kernel\Source;
+
+use Drupal\Tests\ui_patterns\Kernel\SourcePluginsTestBase;
+
+/**
+ * Test SourcePluginManager.
+ *
+ * @group ui_patterns
+ */
+class TextWidgetTest extends SourcePluginsTestBase {
+
+  /**
+   * Test UI Patterns source core plugins.
+   */
+  public function testPlugin(): void {
+    $testData = self::loadTestDataFixture(__DIR__ . "/../../fixtures/TestDataSet.yml");
+    $testSets = $testData->getTestSets();
+    foreach ($testSets as $test_set_name => $test_set) {
+      if (!str_starts_with($test_set_name, 'textfield_')) {
+        continue;
+      }
+      $this->runSourcePluginTest($test_set);
+    }
+  }
+
+}
diff --git a/tests/src/Kernel/Source/TokenSourceTest.php b/tests/src/Kernel/Source/TokenSourceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ae721fcaa3d9f63a9d08e4857d5cdb8d66ada83
--- /dev/null
+++ b/tests/src/Kernel/Source/TokenSourceTest.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\ui_patterns\Kernel\Source;
+
+use Drupal\Tests\ui_patterns\Kernel\SourcePluginsTestBase;
+
+/**
+ * Test SourcePluginManager.
+ *
+ * @group ui_patterns
+ */
+class TokenSourceTest extends SourcePluginsTestBase {
+
+  /**
+   * Test UI Patterns source core plugins.
+   */
+  public function testPlugin(): void {
+    $testData = self::loadTestDataFixture(__DIR__ . "/../../fixtures/TestDataSet.yml");
+    $testSets = $testData->getTestSets();
+    foreach ($testSets as $test_set_name => $test_set) {
+      if (!str_starts_with($test_set_name, 'token_')) {
+        continue;
+      }
+      $this->runSourcePluginTest($test_set);
+    }
+  }
+
+}
diff --git a/tests/src/Kernel/SourcePluginsTestBase.php b/tests/src/Kernel/SourcePluginsTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..37bbdb10fa6af6a6de5214def70052aa3d097f4c
--- /dev/null
+++ b/tests/src/Kernel/SourcePluginsTestBase.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\ui_patterns\Kernel;
+
+use Drupal\Core\Plugin\Context\EntityContext;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\ui_patterns\Traits\RunSourcePluginTestTrait;
+use Drupal\Tests\ui_patterns\Traits\TestContentCreationTrait;
+use Drupal\Tests\ui_patterns\Traits\TestDataTrait;
+
+/**
+ * Base class to test source plugins.
+ *
+ * @group ui_patterns
+ */
+class SourcePluginsTestBase extends KernelTestBase {
+
+  use RunSourcePluginTestTrait;
+  use TestDataTrait;
+  use TestContentCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'system',
+    'user',
+    'text',
+    'field',
+    'node',
+    'ui_patterns',
+    'ui_patterns_test',
+    'datetime',
+    'filter',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    $this->createContentType();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSourceContexts(array $test_set = []): array {
+    $context = [];
+    if (isset($test_set['entity'])) {
+      $entity = $this->createNode('page', is_array($test_set['entity']) ? $test_set['entity'] : []);
+      $context['entity'] = EntityContext::fromEntity($entity);
+    }
+    return $context;
+  }
+
+}
diff --git a/tests/src/Traits/RunSourcePluginTestTrait.php b/tests/src/Traits/RunSourcePluginTestTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..c3723fe0e4f346c96139d93922c9bdb1725a3585
--- /dev/null
+++ b/tests/src/Traits/RunSourcePluginTestTrait.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace Drupal\Tests\ui_patterns\Traits;
+
+use Drupal\Core\Plugin\Context\Context;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\ui_patterns\ComponentPluginManager;
+use Drupal\ui_patterns\SourcePluginBase;
+use Drupal\ui_patterns\SourcePluginManager;
+use function PHPUnit\Framework\assertNotNull;
+use function PHPUnit\Framework\assertTrue;
+
+/**
+ * Run Source with test trait.
+ */
+trait RunSourcePluginTestTrait {
+
+  /**
+   * Return the component manager.
+   */
+  protected function componentManager(): ComponentPluginManager {
+    return \Drupal::service('plugin.manager.sdc');
+  }
+
+  /**
+   * Returns the source plugin manager.
+   */
+  protected function sourcePluginManager(): SourcePluginManager {
+    return \Drupal::service('plugin.manager.ui_patterns_source');
+  }
+
+  /**
+   * Returns the source contexts required by the test.
+   *
+   * Overwrite this function to add additional contexts.
+   *
+   * @param array $test_set
+   *   The test set.
+   *
+   * @return array
+   *   Returns all source contexts required by the test.
+   *
+   * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+   */
+  public function getSourceContexts(array $test_set): array {
+    return [];
+  }
+
+  /**
+   * Returns defined contexts by context id.
+   */
+  private function getContextsByIds(array $test_set): array {
+    $contexts = [];
+    if (isset($test_set['contexts']) && is_array($test_set['contexts'])) {
+      foreach ($test_set['contexts'] as $context_key => $context_value) {
+        $contexts[$context_key] = new Context(new ContextDefinition('any'), $context_value);
+      }
+    }
+    return array_merge($contexts, $this->getSourceContexts($test_set));
+  }
+
+  /**
+   * Assert an expected output.
+   *
+   * @param array $expected_result
+   *   The expected result.
+   * @param string $result
+   *   The result.
+   * @param string $message
+   *   The message.
+   *
+   * @throws \RuntimeException
+   */
+  private function assertExpectedOutput(array $expected_result, mixed $result, string $message = ''): void {
+    if (isset($expected_result['value'])) {
+      $this->assertEquals($expected_result['value'], $result, $message);
+    }
+    elseif (isset($expected_result['regEx'])) {
+      $this->assertTrue(preg_match($expected_result['regEx'], $result) === 1, $message);
+    }
+    else {
+      throw new \RuntimeException(sprintf('Missing "value" or "regEx" in expected result %s', print_r($expected_result, TRUE)));
+    }
+  }
+
+  /**
+   * Get the type definition of a prop or slot.
+   *
+   * @param array $component
+   *   The component definition.
+   * @param string $prop_or_slot_id
+   *   The prop or slot id.
+   *
+   * @return mixed
+   *   The type definition.
+   */
+  private function getComponentPropOrSlotTypeDefinition(array $component, string $prop_or_slot_id): mixed {
+    $type_definition = NULL;
+    if (isset($component['props']['properties'][$prop_or_slot_id])) {
+      $type_definition = $component['props']['properties'][$prop_or_slot_id]['ui_patterns']['type_definition'];
+    }
+    elseif (isset($component['slots'][$prop_or_slot_id])) {
+      $type_definition = $component['slots'][$prop_or_slot_id]['ui_patterns']['type_definition'];
+    }
+    else {
+      throw new \RuntimeException(sprintf("Prop or Slot '%s' not found in component %s", $prop_or_slot_id, $component["id"]));
+    }
+    return $type_definition;
+  }
+
+  /**
+   * Get the source plugin for a prop or slot.
+   *
+   * @param array $component
+   *   The component definition.
+   * @param string $prop_or_slot_id
+   *   The prop or slot id.
+   * @param array $prop_or_slot_configuration
+   *   The prop or slot configuration.
+   * @param string $source_id
+   *   The source id.
+   * @param array $context
+   *   The context.
+   *
+   * @return \Drupal\ui_patterns\SourcePluginBase|null
+   *   The source plugin or NULL.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  private function getSourcePluginForPropOrSlot(array $component, string $prop_or_slot_id, array $prop_or_slot_configuration, string $source_id, array $context): ?SourcePluginBase {
+    $type_definition = $this->getComponentPropOrSlotTypeDefinition($component, $prop_or_slot_id);
+    assertNotNull($type_definition);
+    $configuration = SourcePluginBase::buildConfiguration($prop_or_slot_id, [
+      "ui_patterns" => ["type_definition" => $type_definition],
+    ], $prop_or_slot_configuration, $context);
+    $plugin = $this->sourcePluginManager()->createInstance($source_id, $configuration);
+    return ($plugin instanceof SourcePluginBase) ? $plugin : NULL;
+  }
+
+  /**
+   * Runs source plugin test with given test_set and source.
+   *
+   * @param array $test_set
+   *   The test set.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  public function runSourcePluginTest(array $test_set): void {
+    $component_configuration = $test_set['component'];
+    $component_id = $component_configuration["component_id"];
+    $component = $this->componentManager()->getDefinition($component_id);
+    $contexts = $this->getContextsByIds($test_set);
+    $expected_outputs = $test_set['output'] ?? [];
+    $prop_or_slots = ["props", "slots"];
+    foreach ($prop_or_slots as $prop_or_slot) {
+      if (!isset($expected_outputs[$prop_or_slot], $component_configuration[$prop_or_slot])) {
+        continue;
+      }
+      foreach ($component_configuration[$prop_or_slot] as $prop_or_slot_id => $prop_or_slot_configuration) {
+        if (isset($expected_outputs[$prop_or_slot][$prop_or_slot_id])) {
+          // For slots, there is a table of sources for each slot.
+          $source_to_tests = ($prop_or_slot === "props") ? [$prop_or_slot_configuration] : $prop_or_slot_configuration;
+          $expected_outputs_here = ($prop_or_slot === "props") ? [$expected_outputs[$prop_or_slot][$prop_or_slot_id]] : $expected_outputs[$prop_or_slot][$prop_or_slot_id];
+          foreach ($source_to_tests as $index => $source_to_test) {
+            if (!isset($source_to_test["source_id"])) {
+              throw new \RuntimeException(sprintf("Missing source_id for '%s' in test_set '%s'", $prop_or_slot_id, $test_set["name"] ?? ""));
+            }
+            $plugin = $this->getSourcePluginForPropOrSlot($component, $prop_or_slot_id, $source_to_test, $source_to_test["source_id"], $contexts);
+            assertTrue($plugin instanceof SourcePluginBase);
+            $message = sprintf("Test '%s' failed for prop/slot '%s' of component %s with source '%s' ", $test_set["name"] ?? "", $prop_or_slot_id, $component_id, $source_to_test["source_id"]);
+            $prop_value = $plugin->getPropValue();
+            $this->assertExpectedOutput($expected_outputs_here[$index], $prop_value, $message);
+          }
+        }
+      }
+    }
+  }
+
+}
diff --git a/tests/src/Traits/TestContentCreationTrait.php b/tests/src/Traits/TestContentCreationTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..7c5964490e30c3dde48d2457951a409ce2fb3150
--- /dev/null
+++ b/tests/src/Traits/TestContentCreationTrait.php
@@ -0,0 +1,117 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\ui_patterns\Traits;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\FieldTypePluginManager;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\field\FieldConfigInterface;
+use Drupal\node\Entity\NodeType;
+use Drupal\node\NodeInterface;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+/**
+ * Entity test data trait.
+ */
+trait TestContentCreationTrait {
+
+  use NodeCreationTrait {
+    createNode as drupalCreateNode;
+  }
+
+  /**
+   * Returns the field type plugin manager.
+   */
+  protected function getFieldTypePluginManager(): FieldTypePluginManager {
+    return \Drupal::service('plugin.manager.field.field_type');
+  }
+
+  /**
+   * Set up test node.
+   *
+   * @param string $bundle
+   *   The bundle name.
+   * @param array $values
+   *   Values applied to create method of the node.
+   */
+  private function createNode(string $bundle = 'page', array $values = []): NodeInterface {
+    $this->createContentType($bundle);
+    $node = $this->drupalCreateNode(['type' => $bundle] + $values);
+    $this->drupalCreateNode();
+    return $node;
+  }
+
+  /**
+   * Creates content type with fields foreach field type.
+   *
+   * The created field names are:
+   *   - field_"type" with cardinality -1
+   *   - field_"type"_1 with cardinality 1
+   */
+  protected function createContentType($bundle = 'page'): NodeType {
+    if ($type = NodeType::load($bundle)) {
+      return $type;
+    }
+
+    $type = NodeType::create([
+      'name' => $bundle,
+      'type' => $bundle,
+    ]);
+    $type->save();
+    $field_types = $this->getFieldTypePluginManager()->getDefinitions();
+
+    foreach (array_keys($field_types) as $field_type_id) {
+      if ($field_type_id === 'uuid') {
+        continue;
+      }
+      $field_name = sprintf("field_%s_%s", $field_type_id, 1);
+      $bundle = (string) $type->id();
+      $this->createEntityField($type->getEntityType()->getBundleOf(), $bundle, $field_name, $field_type_id, 1);
+      $field_name = sprintf("field_%s", $field_type_id);
+      $this->createEntityField($type->getEntityType()->getBundleOf(), $bundle, $field_name, $field_type_id,
+          FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+    }
+    return $type;
+  }
+
+  /**
+   * Create a field on a content type.
+   */
+  protected function createEntityField(
+    string $entity_type,
+    string $bundle,
+    string $field_name,
+    string $field_type_id,
+    int $cardinality = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+  ) : FieldConfigInterface {
+    $field_type_definition = $this->getFieldTypePluginManager()->getDefinition($field_type_id);
+    // Create a field storage.
+    $field_storage = [
+      'field_name' => $field_name,
+      'entity_type' => $entity_type,
+      'type' => $field_type_id,
+      'settings' => [],
+      'cardinality' => $cardinality,
+    ];
+    FieldStorageConfig::create($field_storage)->save();
+    // Create a field instance on the content type.
+    $field = [
+      'field_name' => $field_storage['field_name'],
+      'entity_type' => $entity_type,
+      'bundle' => $bundle,
+      'label' => $field_type_definition['label'],
+      'settings' => [],
+    ];
+    $field_config = FieldConfig::create($field);
+    $field_config->save();
+    // Set cardinality.
+    $field_storage_reload = FieldStorageConfig::loadByName($entity_type, $field_name);
+    $field_storage_reload->setCardinality($cardinality);
+    $field_storage_reload->save();
+    return $field_config;
+  }
+
+}
diff --git a/tests/src/Traits/TestDataTrait.php b/tests/src/Traits/TestDataTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..05dee6d987c9e51d682ad941a1a71a9fe0da87c4
--- /dev/null
+++ b/tests/src/Traits/TestDataTrait.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\ui_patterns\Traits;
+
+use Drupal\Component\Serialization\Yaml;
+
+/**
+ * Trait to read fixtures that describe component test situations.
+ */
+trait TestDataTrait {
+
+  /**
+   * Loads test dataset fixture.
+   */
+  protected static function loadTestDataFixture($path) {
+    return new class($path) {
+
+      /**
+       * The loaded fixture.
+       *
+       * @var array
+       */
+      private array $fixture;
+
+      /**
+       * Constructs the TestDataSet.
+       *
+       * @param string $path
+       *   The path to the fixture.
+       */
+      public function __construct(string $path) {
+        $yaml = file_get_contents($path);
+        if ($yaml === FALSE) {
+          throw new \InvalidArgumentException(sprintf("fixture: %s not found.", $path));
+        }
+        $this->fixture = Yaml::decode($yaml);
+      }
+
+      /**
+       * Get test data sets.
+       *
+       * @return array<string, array<string, mixed> >
+       *   The test data sets.
+       */
+      public function getTestSets() : array {
+        if (!is_array($this->fixture)) {
+          return [];
+        }
+        $test_sets = $this->fixture;
+        foreach ($test_sets as $set_name => &$test_set) {
+          $test_set = array_merge(["name" => $set_name], $test_set);
+        }
+        unset($test_set);
+        return $test_sets;
+      }
+
+      /**
+       * Returns data test set from name.
+       *
+       * @param string $set_name
+       *   The test set name.
+       *
+       * @return array
+       *   The test data set.
+       */
+      public function getTestSet(string $set_name): array {
+        $test_sets = $this->getTestSets();
+        if (is_array($test_sets[$set_name])) {
+          return array_merge(["name" => $set_name], $test_sets[$set_name]);
+        }
+        throw new \Exception(sprintf('Test set "%s" not found.', $set_name));
+      }
+
+    };
+  }
+
+}
diff --git a/tests/src/fixtures/TestDataSet.yml b/tests/src/fixtures/TestDataSet.yml
new file mode 100644
index 0000000000000000000000000000000000000000..989be28a019d1fde332518516a7bbddf52495f63
--- /dev/null
+++ b/tests/src/fixtures/TestDataSet.yml
@@ -0,0 +1,58 @@
+---
+textfield_default:
+  component:
+    component_id: ui_patterns_test:test-component
+    props:
+      string:
+        source_id: textfield
+        source:
+          value: 'test input'
+  output:
+    props:
+      string:
+        value: test input
+  entity: {}
+textfield_slot:
+  component:
+    component_id: ui_patterns_test:test-component
+    slots:
+      slot:
+        -
+          source_id: textfield
+          source:
+            value: 'test input'
+  entity: {}
+  output:
+    slots:
+      slot:
+        -
+          value: "test input"
+token_default:
+  component:
+    component_id: ui_patterns_test:test-component
+    props:
+      string:
+        source_id: token
+        source:
+          value: '[node:id]'
+  entity: {}
+  output:
+    props:
+      string:
+        regEx: /[0-9]/
+
+field_property_default:
+  component:
+    component_id: ui_patterns_test:test-component
+    slots:
+      slot:
+        -
+          source_id: field_property:node:field_text_1:value
+  entity:
+    field_text_1:
+      value: 'value_text_1'
+  output:
+    slots:
+      slot:
+        -
+          value: "value_text_1"