diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Plugin/AggregatorPluginSettingsBase.php b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/AggregatorPluginSettingsBase.php
new file mode 100644
index 0000000..d4dcf52
--- /dev/null
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Plugin/AggregatorPluginSettingsBase.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator\Plugin\AggregatorPluginSettingsBase.
+ */
+
+namespace Drupal\aggregator\Plugin;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginBase;
+use Drupal\Core\Form\FormInterface;
+
+/**
+ * Base class for aggregator's plugin implementations.
+ */
+abstract class AggregatorPluginSettingsBase extends ContainerFactoryPluginBase implements FormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'aggregator_admin_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+  }
+
+}
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Tests/AggregatorTestBase.php b/core/modules/aggregator/lib/Drupal/aggregator/Tests/AggregatorTestBase.php
index 0452453..6450975 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Tests/AggregatorTestBase.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Tests/AggregatorTestBase.php
@@ -336,17 +336,17 @@ function getEmptyOpml() {
   }
 
   function getRSS091Sample() {
-    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_rss091.xml';
+    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/modules/aggregator_test/aggregator_test_rss091.xml';
   }
 
   function getAtomSample() {
     // The content of this sample ATOM feed is based directly off of the
     // example provided in RFC 4287.
-    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_atom.xml';
+    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/modules/aggregator_test/aggregator_test_atom.xml';
   }
 
   function getHtmlEntitiesSample() {
-    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_title_entities.xml';
+    return $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'aggregator') . '/tests/modules/aggregator_test/aggregator_test_title_entities.xml';
   }
 
   /**
diff --git a/core/modules/aggregator/tests/Drupal/aggregator/Tests/Plugin/AggregatorPluginSettingsBaseTest.php b/core/modules/aggregator/tests/Drupal/aggregator/Tests/Plugin/AggregatorPluginSettingsBaseTest.php
new file mode 100644
index 0000000..a2d8adf
--- /dev/null
+++ b/core/modules/aggregator/tests/Drupal/aggregator/Tests/Plugin/AggregatorPluginSettingsBaseTest.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator\Tests\Plugin\AggregatorPluginSettingsBaseTest
+ */
+
+namespace Drupal\aggregator\Tests\Plugin;
+
+use Drupal\aggregator\Form\SettingsForm;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests plugins settings on aggregator plugins.
+ *
+ * @group Aggregator
+ */
+class AggregatorPluginSettingsBaseTest extends UnitTestCase {
+
+  /**
+   * The aggregator settings form object under test.
+   *
+   * @var \Drupal\aggregator\Form\SettingsForm
+   */
+  protected $settingsForm;
+
+  /**
+   * The stubbed config factory object.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockBuilder
+   */
+  protected $configFactory;
+
+  /**
+   * The stubbed aggregator plugin managers array.
+   *
+   * @var array
+   */
+  protected $managers;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Aggregator plugin settings tests',
+      'description' => 'Test settings configuration of individual aggregator plugins.',
+      'group' => 'Aggregator',
+    );
+  }
+
+  public function setUp() {
+    $this->configFactory = $this->getConfigFactoryStub(
+      array(
+        'aggregator.settings' => array(
+          'processors' => array('aggregator_test'),
+        ),
+        'aggregator_test.settings' => array(),
+      )
+    );
+    foreach (array('fetcher', 'parser', 'processor') as $type) {
+      $this->managers[$type] = $this->getMockBuilder('Drupal\aggregator\Plugin\AggregatorPluginManager')
+        ->disableOriginalConstructor()
+        ->getMock();
+      $this->managers[$type]->expects($this->once())
+        ->method('getDefinitions')
+        ->will($this->returnValue(array('aggregator_test' => array('title' => '', 'description' => ''))));
+    }
+
+    $this->settingsForm = new SettingsForm(
+      $this->configFactory,
+      $this->getStringTranslationStub(),
+      $this->managers['fetcher'],
+      $this->managers['parser'],
+      $this->managers['processor']
+    );
+  }
+
+  /**
+   * Test for AggregatorPluginSettingsBase.
+   *
+   * Ensure that the settings form calls build, validate and submit methods on
+   * plugins that extend AggregatorPluginSettingsBase.
+   */
+  public function testSettingsForm() {
+    $form_state = array();
+    $test_processor = $this->getMock(
+      'Drupal\aggregator_test\Plugin\aggregator\processor\TestProcessor',
+      array('buildForm', 'validateForm', 'submitForm'),
+      array(array(), 'aggregator_test', array(), $this->configFactory)
+    );
+    $test_processor->expects($this->at(0))
+      ->method('buildForm')
+      ->with($this->anything(), $form_state)
+      ->will($this->returnArgument(0));
+    $test_processor->expects($this->at(1))
+      ->method('validateForm')
+      ->with($this->anything(), $form_state);
+    // @todo Restore when we get rid of drupal_set_message().
+    /*$test_processor->expects($this->at(2))
+      ->method('submitForm')
+      ->with($this->anything(), $form_state);*/
+    $this->managers['processor']->expects($this->once())
+      ->method('createInstance')
+      ->with($this->equalTo('aggregator_test'))
+      ->will($this->returnValue($test_processor));
+    $form = $this->settingsForm->buildForm(array(), $form_state);
+    $this->settingsForm->validateForm($form, $form_state);
+    // @todo Restore when we get rid of drupal_set_message().
+    //$this->settingsForm->submitForm($form, $form_state);
+  }
+
+}
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.info.yml b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.info.yml
new file mode 100644
index 0000000..835d3ed
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.info.yml
@@ -0,0 +1,7 @@
+name: 'Aggregator module tests'
+type: module
+description: 'Support module for aggregator related testing.'
+package: Testing
+version: VERSION
+core: 8.x
+hidden: true
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.module b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.module
new file mode 100644
index 0000000..b18f87b
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test.module
@@ -0,0 +1,74 @@
+<?php
+
+use Drupal\Component\Utility\Crypt;
+
+/**
+ * Implements hook_menu().
+ */
+function aggregator_test_menu() {
+  $items['aggregator/test-feed'] = array(
+    'title' => 'Test feed static last modified date',
+    'description' => "A cached test feed with a static last modified date.",
+    'page callback' => 'aggregator_test_feed',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+  $items['aggregator/redirect'] = array(
+    'title' => 'Test feed with a redirect',
+    'description' => "A feed that redirects to another one",
+    'page callback' => 'aggregator_test_redirect',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
+/**
+ * Page callback. Generates a test feed and simulates last-modified and etags.
+ *
+ * @param $use_last_modified
+ *   Set TRUE to send a last modified header.
+ * @param $use_etag
+ *   Set TRUE to send an etag.
+ */
+function aggregator_test_feed($use_last_modified = FALSE, $use_etag = FALSE) {
+  $last_modified = strtotime('Sun, 19 Nov 1978 05:00:00 GMT');
+  $etag = Crypt::hashBase64($last_modified);
+
+  $if_modified_since = strtotime(Drupal::request()->server->get('HTTP_IF_MODIFIED_SINCE'));
+  $if_none_match = stripslashes(Drupal::request()->server->get('HTTP_IF_NONE_MATCH'));
+
+  // Send appropriate response. We respond with a 304 not modified on either
+  // etag or on last modified.
+  if ($use_last_modified) {
+    drupal_add_http_header('Last-Modified', gmdate(DATE_RFC1123, $last_modified));
+  }
+  if ($use_etag) {
+    drupal_add_http_header('ETag', $etag);
+  }
+  // Return 304 not modified if either last modified or etag match.
+  if ($last_modified == $if_modified_since || $etag == $if_none_match) {
+    drupal_add_http_header('Status', '304 Not Modified');
+    return;
+  }
+
+  // The following headers force validation of cache:
+  drupal_add_http_header('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
+  drupal_add_http_header('Cache-Control', 'must-revalidate');
+  drupal_add_http_header('Content-Type', 'application/rss+xml; charset=utf-8');
+
+  // Read actual feed from file.
+  $file_name = __DIR__ . '/aggregator_test_rss091.xml';
+  $handle = fopen($file_name, 'r');
+  $feed = fread($handle, filesize($file_name));
+  fclose($handle);
+
+  print $feed;
+}
+
+/**
+ * Page callback that redirects to another feed.
+ */
+function aggregator_test_redirect() {
+  drupal_goto('aggregator/test-feed', array(), 301);
+}
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_atom.xml b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_atom.xml
new file mode 100644
index 0000000..357b2e5
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_atom.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+  <title>Example Feed</title>
+  <link href="http://example.org/" />
+  <updated>2003-12-13T18:30:02Z</updated>
+  <author>
+    <name>John Doe</name>
+  </author>
+  <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
+
+  <entry>
+    <title>Atom-Powered Robots Run Amok</title>
+    <link href="http://example.org/2003/12/13/atom03" />
+    <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+    <updated>2003-12-13T18:30:02Z</updated>
+    <summary>Some text.</summary>
+  </entry>
+
+</feed>
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_rss091.xml b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_rss091.xml
new file mode 100644
index 0000000..2944022
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_rss091.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="0.91">
+  <channel>
+    <title>Example</title>
+    <link>http://example.com</link>
+    <description>Example updates</description>
+    <language>en-us</language>
+    <copyright>Copyright 2000, Example team.</copyright>
+    <managingEditor>editor@example.com</managingEditor>
+    <webMaster>webmaster@example.com</webMaster>
+    <image>
+      <title>Example</title>
+      <url>http://example.com/images/druplicon.png</url>
+      <link>http://example.com</link>
+      <width>88</width>
+      <height>100</height>
+      <description>Example updates</description>
+    </image>
+    <item>
+      <title>First example feed item title</title>
+      <link>http://example.com/example-turns-one</link>
+      <description>First example feed item description.</description>
+    </item>
+    <item>
+      <title>Second example feed item title. This title is extremely long so that it exceeds the 255 character limit for titles in feed item storage. In fact it's so long that this sentence isn't long enough so I'm rambling a bit to make it longer, nearly there now. Ah now it's long enough so I'll shut up.</title>
+      <link>http://example.com/example-turns-two</link>
+      <description>Second example feed item description.</description>
+    </item>
+    <item>
+      <title>Long link feed item title.</title>
+      <link>http://example.com/tomorrow/and/tomorrow/and/tomorrow/creeps/in/this/petty/pace/from/day/to/day/to/the/last/syllable/of/recorded/time/and/all/our/yesterdays/have/lighted/fools/the/way/to/dusty/death/out/out/brief/candle/life/is/but/a/walking/shadow/a/poor/player/that/struts/and/frets/his/hour/upon/the/stage/and/is/heard/no/more/it/is/a/tale/told/by/an/idiot/full/of/sound/and/fury/signifying/nothing</link>
+      <description>Long link feed item description.</description>
+    </item>
+    <item>
+      <title>Long author feed item title.</title>
+      <link>http://example.com/long/author</link>
+      <author>I wanted to get out and walk eastward toward the park through the soft twilight, but each time I tried to go I became entangled in some wild, strident argument which pulled me back, as if with ropes, into my chair. Yet high over the city our line of yellow windows must have contributed their share of human secrecy to the casual watcher in the darkening streets, and I was him too, looking up and wondering. I was within and without, simultaneously enchanted and repelled by the inexhaustible variety of life.</author>
+      <description>Long author feed item description.</description>
+    </item>
+  </channel>
+</rss>
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_title_entities.xml b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_title_entities.xml
new file mode 100644
index 0000000..e526e44
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/aggregator_test_title_entities.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="0.91">
+  <channel>
+    <title>Example with Entities</title>
+    <link>http://example.com</link>
+    <description>Example RSS Feed With HTML Entities in Title</description>
+    <language>en-us</language>
+    <item>
+      <title>Quote&quot; Amp&amp;</title>
+      <link>http://example.com/example-turns-one</link>
+      <description>Some text.</description>
+    </item>
+  </channel>
+</rss>
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/config/aggregator_test.settings.yml b/core/modules/aggregator/tests/modules/aggregator_test/config/aggregator_test.settings.yml
new file mode 100644
index 0000000..f858e50
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/config/aggregator_test.settings.yml
@@ -0,0 +1,2 @@
+items:
+  dummy_length: 5
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/fetcher/TestFetcher.php b/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/fetcher/TestFetcher.php
new file mode 100644
index 0000000..8e69f8d
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/fetcher/TestFetcher.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator_test\Plugin\aggregator\fetcher\TestFetcher.
+ */
+
+namespace Drupal\aggregator_test\Plugin\aggregator\fetcher;
+
+use Drupal\aggregator\Plugin\FetcherInterface;
+use Drupal\aggregator\Plugin\aggregator\fetcher\DefaultFetcher;
+use Drupal\aggregator\Plugin\Core\Entity\Feed;
+use Drupal\aggregator\Annotation\AggregatorFetcher;
+use Drupal\Core\Annotation\Translation;
+use Guzzle\Http\Exception\BadResponseException;
+
+/**
+ * Defines a test fetcher implementation.
+ *
+ * Uses http_default_client class to download the feed.
+ *
+ * @AggregatorFetcher(
+ *   id = "aggregator_test_fetcher",
+ *   title = @Translation("Test fetcher"),
+ *   description = @Translation("Dummy fetcher for testing purposes.")
+ * )
+ */
+class TestFetcher extends DefaultFetcher implements FetcherInterface {
+
+  /**
+   * Implements \Drupal\aggregator\Plugin\FetcherInterface::fetch().
+   */
+  public function fetch(Feed $feed) {
+    if ($feed->label() == 'Do not fetch') {
+      return FALSE;
+    }
+    return parent::fetch($feed);
+  }
+}
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/parser/TestParser.php b/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/parser/TestParser.php
new file mode 100644
index 0000000..a9ab57c
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/parser/TestParser.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator_test\Plugin\aggregator\parser\TestParser.
+ */
+
+namespace Drupal\aggregator_test\Plugin\aggregator\parser;
+
+use Drupal\aggregator\Plugin\ParserInterface;
+use Drupal\aggregator\Plugin\Core\Entity\Feed;
+use Drupal\aggregator\Plugin\aggregator\parser\DefaultParser;
+use Drupal\aggregator\Annotation\AggregatorParser;
+use Drupal\Core\Annotation\Translation;
+
+/**
+ * Defines a Test parser implementation.
+ *
+ * Parses RSS, Atom and RDF feeds.
+ *
+ * @AggregatorParser(
+ *   id = "aggregator_test_parser",
+ *   title = @Translation("Test parser"),
+ *   description = @Translation("Dummy parser for testing purposes.")
+ * )
+ */
+class TestParser extends DefaultParser implements ParserInterface {
+
+  /**
+   * Implements \Drupal\aggregator\Plugin\ParserInterface::parse().
+   *
+   * @todo Actually test this.
+   */
+  public function parse(Feed $feed) {
+    return parent::parse($feed);
+  }
+}
diff --git a/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/processor/TestProcessor.php b/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/processor/TestProcessor.php
new file mode 100644
index 0000000..ebbe673
--- /dev/null
+++ b/core/modules/aggregator/tests/modules/aggregator_test/lib/Drupal/aggregator_test/Plugin/aggregator/processor/TestProcessor.php
@@ -0,0 +1,122 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\aggregator_test\Plugin\aggregator\processor\TestProcessor.
+ */
+
+namespace Drupal\aggregator_test\Plugin\aggregator\processor;
+
+use Drupal\aggregator\Plugin\AggregatorPluginSettingsBase;
+use Drupal\aggregator\Plugin\ProcessorInterface;
+use Drupal\aggregator\Plugin\Core\Entity\Feed;
+use Drupal\aggregator\Annotation\AggregatorProcessor;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Config\ConfigFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a default processor implementation.
+ *
+ * Creates lightweight records from feed items.
+ *
+ * @AggregatorProcessor(
+ *   id = "aggregator_test_processor",
+ *   title = @Translation("Test processor"),
+ *   description = @Translation("Test generic processor functionality.")
+ * )
+ */
+class TestProcessor extends AggregatorPluginSettingsBase implements ProcessorInterface {
+
+  /**
+   * Contains the configuration object factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $configFactory;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('config.factory'));
+  }
+
+  /**
+   * Constructs a DefaultProcessor object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param array $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Config\ConfigFactory $config
+   *   The configuration factory object.
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, ConfigFactory $config) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->configFactory = $config;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $processors = $this->configFactory->get('aggregator.settings')->get('processors');
+    $info = $this->getDefinition();
+
+    $form['processors'][$info['id']] = array(
+      '#type' => 'details',
+      '#title' => t('Test processor settings'),
+      '#description' => $info['description'],
+      '#collapsed' => !in_array($info['id'], $processors),
+    );
+    // Add some dummy settings to verify settingsForm is called.
+    $form['processors'][$info['id']]['dummy_length'] = array(
+      '#title' => t('Dummy length setting'),
+      '#type' => 'number',
+      '#min' => 1,
+      '#max' => 1000,
+      '#default_value' => $this->configFactory->get('aggregator_test.settings')->get('items.dummy_length'),
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $this->configFactory->get('aggregator_test.settings')
+      ->set('items.dummy_length', $form_state['values']['dummy_length'])
+      ->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process(Feed $feed) {
+    foreach ($feed->items as &$item) {
+      // Prepend our test string.
+      $item['title'] = 'testProcessor' . $item['title'];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function remove(Feed $feed) {
+    // Append a random number, just to change the feed description.
+    $feed->description->value .= rand(0, 10);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postProcess(Feed $feed) {
+    // Double the refresh rate.
+    $feed->refresh->value *= 2;
+    $feed->save();
+  }
+
+}
