diff --git a/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php
index 60df068..db9bf76 100644
--- a/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php
+++ b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php
@@ -175,6 +175,16 @@ class Migration extends ConfigEntityBase implements MigrationInterface {
   public $sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
 
   /**
+   * The fraction of the memory limit at which an operation will be interrupted.
+   *
+   * Can be overridden by a Migration subclass if one would like to push the
+   * envelope. Defaults to 85%.
+   *
+   * @var float
+   */
+  protected $memoryThreshold = 0.85;
+
+  /**
    * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
    */
   protected $highwaterStorage;
@@ -185,6 +195,23 @@ class Migration extends ConfigEntityBase implements MigrationInterface {
   public $trackLastImported = FALSE;
 
   /**
+   * The fraction of the time limit at which an operation will be interrupted.
+   *
+   * Can be overridden by a Migration subclass if one would like to push the
+   * envelope. Defaults to 90%.
+   *
+   * @var float
+   */
+  public $timeThreshold = 0.90;
+
+  /**
+   * The time limit when executing the migration.
+   *
+   * @var array
+   */
+  public $limit = array();
+
+  /**
    * {@inheritdoc}
    */
   public function getSourcePlugin() {
diff --git a/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php
index 2b4f181..557b76b 100644
--- a/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php
+++ b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php
@@ -27,14 +27,14 @@ class MigrateExecutable {
    *
    * @var int
    */
-  protected $successes_since_feedback;
+  protected $successesSinceFeedback;
 
   /**
    * The number of rows that were successfully processed.
    *
    * @var int
    */
-  protected $total_successes;
+  protected $totalSuccesses;
 
   /**
    * Status of one row.
@@ -53,7 +53,7 @@ class MigrateExecutable {
    *
    * @var int
    */
-  protected $total_processed;
+  protected $totalProcessed;
 
   /**
    * The queued messages not yet saved.
@@ -77,38 +77,11 @@ class MigrateExecutable {
   protected $options;
 
   /**
-   * The fraction of the memory limit at which an operation will be interrupted.
-   *
-   * Can be overridden by a Migration subclass if one would like to push the
-   * envelope. Defaults to 85%.
-   *
-   * @var float
-   */
-  protected $memoryThreshold = 0.85;
-
-  /**
-   * The PHP memory_limit expressed in bytes.
-   *
-   * @var int
-   */
-  protected $memoryLimit;
-
-  /**
-   * The fraction of the time limit at which an operation will be interrupted.
-   *
-   * Can be overridden by a Migration subclass if one would like to push the
-   * envelope. Defaults to 90%.
-   *
-   * @var float
-   */
-  protected $timeThreshold = 0.90;
-
-  /**
    * The PHP max_execution_time.
    *
    * @var int
    */
-  protected $timeLimit;
+  protected $maxExecTime;
 
   /**
    * The configuration values of the source.
@@ -122,7 +95,14 @@ class MigrateExecutable {
    *
    * @var int
    */
-  protected $processed_since_feedback = 0;
+  protected $processedSinceFeedback = 0;
+
+  /**
+   * The PHP memory_limit expressed in bytes.
+   *
+   * @var int
+   */
+  protected $memoryLimit;
 
   /**
    * The translation manager.
@@ -168,6 +148,8 @@ public function __construct(MigrationInterface $migration, MigrateMessageInterfa
       }
       $this->memoryLimit = $limit;
     }
+    // Record the maximum execution time limit.
+    $this->maxExecTime = ini_get('max_execution_time');
   }
 
   /**
@@ -237,11 +219,12 @@ public function import() {
           array('!e' => $e->getMessage())));
       return MigrationInterface::RESULT_FAILED;
     }
+
     while ($source->valid()) {
       $row = $source->current();
       if ($this->sourceIdValues = $row->getSourceIdValues()) {
         // Wipe old messages, and save any new messages.
-        $id_map->delete($row->getSourceIdValues(), TRUE);
+        $id_map->delete($this->sourceIdValues, TRUE);
         $this->saveQueuedMessages();
       }
 
@@ -252,12 +235,12 @@ public function import() {
         // @TODO handle the successful but no ID case like config.
         if ($destination_id_values) {
           $id_map->saveIdMapping($row, $destination_id_values, $this->sourceRowStatus, $this->rollbackAction);
-          $this->successes_since_feedback++;
-          $this->total_successes++;
+          $this->successesSinceFeedback++;
+          $this->totalSuccesses++;
         }
         else {
           $id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction);
-          if ($id_map->messageCount() == 0) {
+          if (!$id_map->messageCount()) {
             $message = $this->t('New object was not saved, no error provided');
             $this->saveMessage($message);
             $this->message->display($message);
@@ -273,8 +256,8 @@ public function import() {
         $this->migration->getIdMap()->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction);
         $this->handleException($e);
       }
-      $this->total_processed++;
-      $this->processed_since_feedback++;
+      $this->totalProcessed++;
+      $this->processedSinceFeedback++;
       if ($highwater_property = $this->migration->get('highwaterProperty')) {
         $this->migration->saveHighwater($row->getSourceProperty($highwater_property['name']));
       }
@@ -438,7 +421,7 @@ protected function checkStatus() {
     if ($this->memoryExceeded()) {
       return MigrationInterface::RESULT_INCOMPLETE;
     }
-    if ($this->timeExceeded()) {
+    if ($this->maxExecTimeExceeded()) {
       return MigrationInterface::RESULT_INCOMPLETE;
     }
     /*
@@ -468,28 +451,28 @@ protected function checkStatus() {
    *  TRUE if the threshold is exceeded, FALSE if not.
    */
   protected function memoryExceeded() {
-    $usage = memory_get_usage();
+    $usage = $this->getMemoryUsage();
     $pct_memory = $usage / $this->memoryLimit;
-    if ($pct_memory > $this->memoryThreshold) {
+    if (!$threshold = $this->migration->get('memoryThreshold')) {
+      return FALSE;
+    }
+    if ($pct_memory > $threshold) {
       $this->message->display(
-        $this->t('Memory usage is !usage (!pct% of limit !limit), resetting statics',
+        $this->t('Memory usage is !usage (!pct% of limit !limit), reclaiming memory.',
           array('!pct' => round($pct_memory*100),
-                '!usage' => format_size($usage),
-                '!limit' => format_size($this->memoryLimit))),
+                '!usage' => $this->formatSize($usage),
+                '!limit' => $this->formatSize($this->memoryLimit))),
         'warning');
-      // First, try resetting Drupal's static storage - this frequently releases
-      // plenty of memory to continue
-      drupal_static_reset();
-      $usage = memory_get_usage();
-      $pct_memory = $usage/$this->memoryLimit;
+      $usage = $this->attemptMemoryReclaim();
+      $pct_memory = $usage / $this->memoryLimit;
       // Use a lower threshold - we don't want to be in a situation where we keep
       // coming back here and trimming a tiny amount
-      if ($pct_memory > (.90 * $this->memoryThreshold)) {
+      if ($pct_memory > (.90 * $threshold)) {
         $this->message->display(
           $this->t('Memory usage is now !usage (!pct% of limit !limit), not enough reclaimed, starting new batch',
             array('!pct' => round($pct_memory*100),
-                  '!usage' => format_size($usage),
-                  '!limit' => format_size($this->memoryLimit))),
+                  '!usage' => $this->formatSize($usage),
+                  '!limit' => $this->formatSize($this->memoryLimit))),
           'warning');
         return TRUE;
       }
@@ -497,8 +480,8 @@ protected function memoryExceeded() {
         $this->message->display(
           $this->t('Memory usage is now !usage (!pct% of limit !limit), reclaimed enough, continuing',
             array('!pct' => round($pct_memory*100),
-                  '!usage' => format_size($usage),
-                  '!limit' => format_size($this->memoryLimit))),
+                  '!usage' => $this->formatSize($usage),
+                  '!limit' => $this->formatSize($this->memoryLimit))),
           'warning');
         return FALSE;
       }
@@ -509,23 +492,65 @@ protected function memoryExceeded() {
   }
 
   /**
-   * Test whether we're approaching the PHP time limit.
+   * Returns the memory usage so far.
+   *
+   * @return int
+   */
+  protected function getMemoryUsage() {
+    return memory_get_usage();
+  }
+
+  /**
+   * Try to reclaim memory.
+   *
+   * @return int
+   *   The memory usage after reclaim.
+   */
+  protected function attemptMemoryReclaim() {
+     // First, try resetting Drupal's static storage - this frequently releases
+    // plenty of memory to continue
+    drupal_static_reset();
+    // @TODO: explore kernel reset.
+    return memory_get_usage();
+  }
+
+  /**
+   * Generates a string representation for the given byte count.
+   *
+   * @param $size
+   *   A size in bytes.
+   *
+   * @return
+   *   A translated string representation of the size.
+   */
+  protected function formatSize($size) {
+    return format_size($size);
+  }
+
+  /**
+   * Test whether we're approaching the PHP maximum execution time limit.
    *
    * @return boolean
    *  TRUE if the threshold is exceeded, FALSE if not.
    */
-  protected function timeExceeded() {
-    if ($this->timeLimit == 0) {
-      return FALSE;
-    }
-    $time_elapsed = time() - REQUEST_TIME;
-    $pct_time = $time_elapsed / $this->timeLimit;
-    if ($pct_time > $this->timeThreshold) {
-      return TRUE;
-    }
-    else {
-      return FALSE;
+  protected function maxExecTimeExceeded() {
+    if ($this->maxExecTime) {
+      $time_elapsed = $this->getTimeElapsed();
+      $pct_time = $time_elapsed / $this->maxExecTime;
+      if ($pct_time > $this->migration->get('timeThreshold')) {
+        return TRUE;
+      }
     }
+    return FALSE;
+  }
+
+  /**
+   * Returns the time elapsed.
+   *
+   * This allows a test to set a fake elapsed time.
+   */
+  protected function getTimeElapsed() {
+    return time() - REQUEST_TIME;
   }
 
   /**
diff --git a/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php
index 44df10c..bbfc448 100644
--- a/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php
+++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php
@@ -381,7 +381,7 @@ public function lookupSourceID(array $destination_id) {
     }
     $result = $query->execute();
     $source_id = $result->fetchAssoc();
-    return array_values($source_id);
+    return array_values($source_id ?: array());
   }
 
   /**
@@ -395,7 +395,7 @@ public function lookupDestinationID(array $source_id) {
     }
     $result = $query->execute();
     $destination_id = $result->fetchAssoc();
-    return array_values($destination_id);
+    return array_values($destination_id ?: array());
   }
 
   /**
@@ -444,8 +444,8 @@ public function saveMessage(array $source_id_values, $message, $level = Migratio
     $count = 1;
     foreach ($source_id_values as $id_value) {
       $fields['sourceid' . $count++] = $id_value;
-      // If any key value is empty, we can't save - print out and abort.
-      if (empty($id_value)) {
+      // If any key value is not set, we can't save - print out and abort.
+      if (!isset($id_value)) {
         print($message);
         return;
       }
@@ -579,7 +579,7 @@ public function deleteDestination(array $destination_id) {
    * {@inheritdoc}
    */
   public function setUpdate(array $source_id) {
-    if (empty($source_ids)) {
+    if (empty($source_id)) {
       throw new MigrateException('No source identifiers provided to update.');
     }
     $query = $this->getDatabase()
@@ -695,16 +695,13 @@ public function key() {
    * from rewind().
    */
   public function next() {
-    $this->currentRow = $this->result->fetchObject();
+    $this->currentRow = $this->result->fetchAssoc();
     $this->currentKey = array();
-    if (!is_object($this->currentRow)) {
-      $this->currentRow = NULL;
-    }
-    else {
+    if ($this->currentRow) {
       foreach ($this->sourceIdFields as $map_field) {
-        $this->currentKey[$map_field] = $this->currentRow->$map_field;
+        $this->currentKey[$map_field] = $this->currentRow[$map_field];
         // Leave only destination fields.
-        unset($this->currentRow->$map_field);
+        unset($this->currentRow[$map_field]);
       }
     }
   }
@@ -717,7 +714,7 @@ public function next() {
    */
   public function valid() {
     // @todo Check numProcessed against itemlimit.
-    return !is_null($this->currentRow);
+    return $this->currentRow !== FALSE;
   }
 
 }
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/ConfigDestinationTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/ConfigDestinationTest.php
new file mode 100644
index 0000000..10d7353
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/ConfigDestinationTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\ConfigDestinationTest.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\migrate\Plugin\migrate\destination\Config;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ *
+ * @group migrate
+ */
+class ConfigDestinationTest extends UnitTestCase {
+
+  public function testImport() {
+    $source = array(
+      'test' => 'x',
+    );
+    $config = $this->getMockBuilder('Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $config->expects($this->once())
+      ->method('setData')
+      ->with($this->equalTo($source))
+      ->will($this->returnValue($config));
+    $config->expects($this->once())
+      ->method('save');
+    $row = $this->getMockBuilder('Drupal\migrate\Row')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $row->expects($this->once())
+      ->method('getDestination')
+      ->will($this->returnValue($source));
+    $destination = new Config(array(), 'd8_config', array('pluginId' => 'd8_config'), $config);
+    $destination->import($row);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Destination test',
+      'description' => 'Tests for destination plugin.',
+      'group' => 'Migrate',
+    );
+  }
+
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php
new file mode 100644
index 0000000..36f8150
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\FakeConnection.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\Database\Connection;
+
+class FakeConnection extends Connection {
+
+  /**
+   * @var string
+   */
+  protected $tablePrefix;
+
+  /**
+   * @var array
+   */
+  protected $connectionOptions;
+
+  public function __construct($database_contents, $connection_options = array(), $prefix = '') {
+    $this->databaseContents = $database_contents;
+    $this->connectionOptions = $connection_options;
+    $this->tablePrefix = $prefix;
+  }
+
+  public function select($table, $alias = NULL, array $options = array()) {
+    return new FakeSelect($this->databaseContents, $table, $alias);
+  }
+
+  public function schema() {
+    return new FakeDatabaseSchema($this->databaseContents);
+  }
+
+  public function insert($table, array $options = array()) {
+    return new FakeInsert($this->databaseContents, $table);
+  }
+
+  public function update($table, array $options = array()) {
+    return new FakeUpdate($this->databaseContents, $table);
+  }
+
+  public function merge($table, array $options = array()) {
+    return new FakeMerge($this->databaseContents, $table);
+  }
+
+  public function truncate($table, array $options = array()) {
+    return new FakeTruncate($this->databaseContents, $table);
+  }
+
+  public function tablePrefix($table = 'default') {
+    return $this->tablePrefix;
+  }
+
+  public function getConnectionOptions() {
+    return $this->connectionOptions;
+  }
+
+  public function query($query, array $args = array(), $options = array()) {
+    throw new \Exception('Method not supported');
+  }
+
+  public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
+    throw new \Exception('Method not supported');
+  }
+
+  public function queryTemporary($query, array $args = array(), array $options = array()) {
+    throw new \Exception('Method not supported');
+  }
+
+  public function driver() {
+    throw new \Exception('Method not supported');
+  }
+
+  public function databaseType() {
+    throw new \Exception('Method not supported');
+  }
+
+  public function createDatabase($database) {
+    // There is nothing to do.
+  }
+
+  public function mapConditionOperator($operator) {
+    throw new \Exception('Method not supported');
+  }
+
+  public function nextId($existing_id = 0) {
+    throw new \Exception('Method not supported');
+  }
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php
index bab582f..7cf32d1 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php
@@ -19,11 +19,11 @@ class FakeDatabaseSchema extends Schema {
    */
   public $databaseContents;
 
-  public function __construct($database_contents) {
+  public function __construct(&$database_contents) {
     $this->uniqueIdentifier = uniqid('', TRUE);
 
     // @todo Maybe we can generate an internal representation.
-    $this->databaseContents = $database_contents;
+    $this->databaseContents = &$database_contents;
   }
 
   public function tableExists($table) {
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php
new file mode 100644
index 0000000..a4631a9
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\FakeInsert.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\Database\Query\Insert;
+use Drupal\Core\Database\Query\SelectInterface;
+
+class FakeInsert extends Insert {
+
+  /**
+   * @var array
+   */
+  protected $databaseContents;
+
+  /**
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * Constructs a fake insert object.
+   * @param \Drupal\Core\Database\Connection $database_contents
+   * @param string $table
+   * @param array $options
+   */
+  public function __construct(&$database_contents, $table, array $options = array()) {
+    $this->databaseContents = &$database_contents;
+    $this->table = $table;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function useDefaults(array $fields) {
+    throw new \Exception('This method is not supported');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function from(SelectInterface $query) {
+    throw new \Exception('This method is not supported');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    foreach ($this->insertValues as $values) {
+      $this->databaseContents[$this->table][] = array_combine($this->insertFields, $values);
+    }
+  }
+
+
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php
new file mode 100644
index 0000000..12e35c4
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\FakeMerge.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\Database\Query\Condition;
+use Drupal\Core\Database\Query\InvalidMergeQueryException;
+use Drupal\Core\Database\Query\Merge;
+
+class FakeMerge extends Merge {
+
+
+  function __construct(&$database_contents, $table) {
+    $this->databaseContents = &$database_contents;
+    $this->table = $table;
+    $this->conditionTable = $table;
+    $this->condition = new Condition('AND');
+  }
+
+  public function execute() {
+    if (!count($this->condition)) {
+      throw new InvalidMergeQueryException(t('Invalid merge query: no conditions'));
+    }
+    $select = new FakeSelect($this->databaseContents, $this->conditionTable, 'c');
+    $count = $select
+      ->condition($this->condition)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    if ($count) {
+      $update = new FakeUpdate($this->databaseContents, $this->table);
+      $update
+        ->fields($this->updateFields)
+        ->condition($this->condition)
+        ->execute();
+      return self::STATUS_UPDATE;
+    }
+    $insert = new FakeInsert($this->databaseContents, $this->table);
+    $insert->fields($this->insertFields)->execute();
+    return self::STATUS_INSERT;
+  }
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php
index 6bf586e..7dee544 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php
@@ -51,21 +51,20 @@ class FakeSelect extends Select {
   /**
    * Constructs a new FakeSelect.
    *
+   * @param array $database_contents
+   *   An array of mocked database content.
    * @param string $table
    *   The base table name used within fake select.
    * @param string $alias
    *   The base table alias used within fake select.
-   * @param array $database_contents
-   *   An array of mocked database content.
-   *
    * @param string $conjunction
    *   The operator to use to combine conditions: 'AND' or 'OR'.
    */
-  public function __construct($table, $alias, array $database_contents, $conjunction = 'AND') {
+  public function __construct(array $database_contents, $table, $alias, $conjunction = 'AND') {
+    $this->databaseContents = $database_contents;
     $this->addJoin(NULL, $table, $alias);
     $this->where = new Condition($conjunction);
     $this->having = new Condition($conjunction);
-    $this->databaseContents = $database_contents;
   }
 
   /**
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php
index 9088c50..4fadf2f 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php
@@ -49,8 +49,11 @@ public function fetchField($index = 0) {
    * {@inheritdoc}
    */
   public function fetchAssoc() {
-    $return = $this->current();
-    $this->next();
+    $return = FALSE;
+    if ($this->valid()) {
+      $return = $this->current();
+      $this->next();
+    }
     return $return;
   }
 
@@ -92,6 +95,14 @@ public function fetchAllAssoc($key, $fetch = NULL) {
   /**
    * {@inheritdoc}
    */
+  public function fetchObject() {
+    $return = $this->fetchAssoc();
+    return $return === FALSE ? FALSE : (object) $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public static function getInfo() {
     return array(
       'name' => 'Fake statement test',
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php
new file mode 100644
index 0000000..15c7ea6
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\FakeTruncate.
+ */
+
+namespace Drupal\migrate\Tests;
+
+class FakeTruncate {
+
+  public function __construct(&$database_contents, $table) {
+    $this->databaseContents = &$database_contents;
+    $this->table = $table;
+  }
+
+  public function execute() {
+    $this->databaseContents[$this->table] = array();
+  }
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php
new file mode 100644
index 0000000..2e83a34
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\FakeUpdate.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\Database\Query\Condition;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\Query\Update;
+
+class FakeUpdate extends Update {
+
+  /**
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * @var array
+   */
+  protected $databaseContents;
+
+  /**
+   * Constructs a FakeUpdate object.
+   * @param \Drupal\Core\Database\Connection $database_contents
+   * @param string $table
+   */
+  public function __construct(&$database_contents, $table) {
+    $this->table = $table;
+    $this->condition = new Condition('AND');
+    $this->databaseContents = &$database_contents;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $affected = 0;
+    if (isset($this->databaseContents[$this->table])) {
+      $fields = $this->fields;
+      $condition = $this->condition;
+      array_walk($this->databaseContents[$this->table], function (&$row_array) use ($fields, $condition, &$affected) {
+        $row = new DatabaseRow($row_array);
+        if (ConditionResolver::matchGroup($row, $condition)) {
+          $row_array = $fields + $row_array;
+          $affected++;
+        }
+      });
+    }
+    return $affected;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exists(SelectInterface $select) {
+    throw new \Exception(sprintf('Method "%s" is not supported', __METHOD__));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function where($snippet, $args = array()) {
+    throw new \Exception(sprintf('Method "%s" is not supported', __METHOD__));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function expression($field, $expression, array $arguments = NULL) {
+    throw new \Exception(sprintf('Method "%s" is not supported', __METHOD__));
+  }
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecutableTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecutableTest.php
index 80beaac..77b25fd 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecutableTest.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecutableTest.php
@@ -7,9 +7,10 @@
 
 namespace Drupal\migrate\Tests;
 
-use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\migrate\Entity\MigrationInterface;
-use Drupal\migrate\MigrateExecutable;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Row;
 
 /**
  * Tests the migrate executable.
@@ -38,7 +39,7 @@ class MigrateExecutableTest extends MigrateTestCase {
   /**
    * The tested migrate executable.
    *
-   * @var \Drupal\migrate\MigrateExecutable
+   * @var \Drupal\migrate\Tests\TestMigrateExecutable
    */
   protected $executable;
 
@@ -46,7 +47,8 @@ class MigrateExecutableTest extends MigrateTestCase {
 
   protected $migrationConfiguration = array(
     'id' => 'test',
-    'limit' => array('units' => 'seconds', 'value' => 5),
+    'limit' => array('unit' => 'second', 'value' => 1),
+    'timeThreshold' => 0.9,
   );
 
   /**
@@ -60,14 +62,13 @@ public static function getInfo() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
+    parent::setUp();
     $this->migration = $this->getMigration();
     $this->message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
-    $id_map = $this->getMock('Drupal\migrate\Plugin\MigrateIdMapInterface');
-
-    $this->migration->expects($this->any())
-      ->method('getIdMap')
-      ->will($this->returnValue($id_map));
 
     $this->executable = new TestMigrateExecutable($this->migration, $this->message);
     $this->executable->setTranslationManager($this->getStringTranslationStub());
@@ -80,10 +81,8 @@ public function testImportWithFailingRewind() {
     $iterator = $this->getMock('\Iterator');
     $exception_message = $this->getRandomGenerator()->string();
     $iterator->expects($this->once())
-      ->method('valid')
-      ->will($this->returnCallback(function() use ($exception_message) {
-        throw new \Exception($exception_message);
-      }));
+      ->method('rewind')
+      ->will($this->throwException(new \Exception($exception_message)));
     $source = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface');
     $source->expects($this->any())
       ->method('getIterator')
@@ -102,11 +101,353 @@ public function testImportWithFailingRewind() {
     $this->assertEquals(MigrationInterface::RESULT_FAILED, $result);
   }
 
-}
+  /**
+   * Tests the import method with a valid row.
+   */
+  public function testImportWithValidRow() {
+    $source = $this->getMockSource();
 
-class TestMigrateExecutable extends MigrateExecutable {
+    $row = $this->getMockBuilder('Drupal\migrate\Row')
+      ->disableOriginalConstructor()
+      ->getMock();
 
-  public function setTranslationManager(TranslationInterface $translation_manager) {
-    $this->translationManager = $translation_manager;
+    $source->expects($this->once())
+      ->method('current')
+      ->will($this->returnValue($row));
+
+    $this->executable->setSource($source);
+
+    $this->migration->expects($this->once())
+      ->method('getProcessPlugins')
+      ->will($this->returnValue(array()));
+
+    $destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
+    $destination->expects($this->once())
+      ->method('import')
+      ->with($row)
+      ->will($this->returnValue(array('id' => 'test')));
+
+    $this->migration->expects($this->once())
+      ->method('getDestinationPlugin')
+      ->will($this->returnValue($destination));
+
+    $this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
+
+    $this->assertSame(1, $this->executable->getSuccessesSinceFeedback());
+    $this->assertSame(1, $this->executable->getTotalSuccesses());
+    $this->assertSame(1, $this->executable->getTotalProcessed());
+    $this->assertSame(1, $this->executable->getProcessedSinceFeedback());
   }
+
+  /**
+   * Tests the import method with a valid row.
+   */
+  public function testImportWithValidRowNoDestinationValues() {
+    $source = $this->getMockSource();
+
+    $row = $this->getMockBuilder('Drupal\migrate\Row')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $row->expects($this->once())
+      ->method('getSourceIdValues')
+      ->will($this->returnValue(array('id' => 'test')));
+
+    $source->expects($this->once())
+      ->method('current')
+      ->will($this->returnValue($row));
+
+    $this->executable->setSource($source);
+
+    $this->migration->expects($this->once())
+      ->method('getProcessPlugins')
+      ->will($this->returnValue(array()));
+
+    $destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
+    $destination->expects($this->once())
+      ->method('import')
+      ->with($row)
+      ->will($this->returnValue(array()));
+
+    $this->migration->expects($this->once())
+      ->method('getDestinationPlugin')
+      ->will($this->returnValue($destination));
+
+    $this->idMap->expects($this->once())
+      ->method('delete')
+      ->with(array('id' => 'test'), TRUE);
+
+    $this->idMap->expects($this->once())
+      ->method('saveIdMapping')
+      ->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL);
+
+    $this->idMap->expects($this->once())
+      ->method('messageCount')
+      ->will($this->returnValue(0));
+
+    $this->idMap->expects($this->once())
+      ->method('saveMessage');
+
+    $this->message->expects($this->once())
+      ->method('display')
+      ->with('New object was not saved, no error provided');
+
+    $this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
+  }
+
+  /**
+   * Tests the import method with a MigrateException being thrown.
+   */
+  public function testImportWithValidRowWithMigrateException() {
+    $exception_message = $this->getRandomGenerator()->string();
+    $source = $this->getMockSource();
+
+    $row = $this->getMockBuilder('Drupal\migrate\Row')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $row->expects($this->once())
+      ->method('getSourceIdValues')
+      ->will($this->returnValue(array('id' => 'test')));
+
+    $source->expects($this->once())
+      ->method('current')
+      ->will($this->returnValue($row));
+
+    $this->executable->setSource($source);
+
+    $this->migration->expects($this->once())
+      ->method('getProcessPlugins')
+      ->will($this->returnValue(array()));
+
+    $destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
+    $destination->expects($this->once())
+      ->method('import')
+      ->with($row)
+      ->will($this->throwException(new MigrateException($exception_message)));
+
+    $this->migration->expects($this->once())
+      ->method('getDestinationPlugin')
+      ->will($this->returnValue($destination));
+
+    $this->idMap->expects($this->once())
+      ->method('saveIdMapping')
+      ->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL);
+
+    $this->idMap->expects($this->once())
+      ->method('saveMessage');
+
+    $this->message->expects($this->once())
+      ->method('display')
+      ->with($exception_message);
+
+    $this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
+  }
+
+  /**
+   * Tests the import method with a regular Exception being thrown.
+   */
+  public function testImportWithValidRowWithException() {
+    $exception_message = $this->getRandomGenerator()->string();
+    $source = $this->getMockSource();
+
+    $row = $this->getMockBuilder('Drupal\migrate\Row')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $row->expects($this->once())
+      ->method('getSourceIdValues')
+      ->will($this->returnValue(array('id' => 'test')));
+
+    $source->expects($this->once())
+      ->method('current')
+      ->will($this->returnValue($row));
+
+    $this->executable->setSource($source);
+
+    $this->migration->expects($this->once())
+      ->method('getProcessPlugins')
+      ->will($this->returnValue(array()));
+
+    $destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
+    $destination->expects($this->once())
+      ->method('import')
+      ->with($row)
+      ->will($this->throwException(new \Exception($exception_message)));
+
+    $this->migration->expects($this->once())
+      ->method('getDestinationPlugin')
+      ->will($this->returnValue($destination));
+
+    $this->idMap->expects($this->once())
+      ->method('saveIdMapping')
+      ->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL);
+
+    $this->idMap->expects($this->once())
+      ->method('saveMessage');
+
+    $this->message->expects($this->once())
+      ->method('display')
+      ->with($exception_message);
+
+    $this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
+  }
+
+  /**
+   * Tests time limit option method.
+   */
+  public function testTimeOptionExceeded() {
+    // Assert time limit of one second (test configuration default) is exceeded.
+    $this->assertTrue($this->executable->timeOptionExceeded());
+    // Assert time limit not exceeded.
+    $this->migration->set('limit', array('unit' => 'seconds', 'value' => (REQUEST_TIME - 3600)));
+    $this->assertFalse($this->executable->timeOptionExceeded());
+    // Assert no time limit.
+    $this->migration->set('limit', array());
+    $this->assertFalse($this->executable->timeOptionExceeded());
+  }
+
+  /**
+   * Tests get time limit method.
+   */
+  public function testGetTimeLimit() {
+    // Assert time limit has a unit of one second (test configuration default).
+    $limit = $this->migration->get('limit');
+    $this->assertArrayHasKey('unit', $limit);
+    $this->assertSame('second', $limit['unit']);
+    $this->assertSame($limit['value'], $this->executable->getTimeLimit());
+    // Assert time limit has a unit of multiple seconds.
+    $this->migration->set('limit', array('unit' => 'seconds', 'value' => 30));
+    $limit = $this->migration->get('limit');
+    $this->assertArrayHasKey('unit', $limit);
+    $this->assertSame('seconds', $limit['unit']);
+    $this->assertSame($limit['value'], $this->executable->getTimeLimit());
+    // Assert no time limit.
+    $this->migration->set('limit', array());
+    $limit = $this->migration->get('limit');
+    $this->assertArrayNotHasKey('unit', $limit);
+    $this->assertArrayNotHasKey('value', $limit);
+    $this->assertNull($this->executable->getTimeLimit());
+  }
+
+  /**
+   * Tests saving of queued messages.
+   */
+  public function testSaveQueuedMessages() {
+    // Assert no queued messages before save.
+    $this->assertAttributeEquals(array(), 'queuedMessages', $this->executable);
+    // Set required source_id_values for MigrateIdMapInterface::saveMessage().
+    $expected_messages[] = array('message' => 'message 1', 'level' => MigrationInterface::MESSAGE_ERROR);
+    $expected_messages[] = array('message' => 'message 2', 'level' => MigrationInterface::MESSAGE_WARNING);
+    $expected_messages[] = array('message' => 'message 3', 'level' => MigrationInterface::MESSAGE_INFORMATIONAL);
+    foreach ($expected_messages as $queued_message) {
+      $this->executable->queueMessage($queued_message['message'], $queued_message['level']);
+    }
+    $this->executable->setSourceIdValues(array());
+    $this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable);
+    // No asserts of saved messages since coverage exists
+    // in MigrateSqlIdMapTest::saveMessage().
+    $this->executable->saveQueuedMessages();
+    // Assert no queued messages after save.
+    $this->assertAttributeEquals(array(), 'queuedMessages', $this->executable);
+  }
+
+  /**
+   * Tests the queuing of messages.
+   */
+  public function testQueueMessage() {
+    // Assert no queued messages.
+    $expected_messages = array();
+    $this->assertAttributeEquals(array(), 'queuedMessages', $this->executable);
+    // Assert a single (default level) queued message.
+    $expected_messages[] = array(
+      'message' => 'message 1',
+      'level' => MigrationInterface::MESSAGE_ERROR,
+    );
+    $this->executable->queueMessage('message 1');
+    $this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable);
+    // Assert multiple queued messages.
+    $expected_messages[] = array(
+      'message' => 'message 2',
+      'level' => MigrationInterface::MESSAGE_WARNING,
+    );
+    $this->executable->queueMessage('message 2', MigrationInterface::MESSAGE_WARNING);
+    $this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable);
+    $expected_messages[] = array(
+      'message' => 'message 3',
+      'level' => MigrationInterface::MESSAGE_INFORMATIONAL,
+    );
+    $this->executable->queueMessage('message 3', MigrationInterface::MESSAGE_INFORMATIONAL);
+    $this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable);
+  }
+
+  /**
+   * Tests maximum execution time (max_execution_time) of an import.
+   */
+  public function testMaxExecTimeExceeded() {
+    // Assert no max_execution_time value.
+    $this->executable->setMaxExecTime(0);
+    $this->assertFalse($this->executable->maxExecTimeExceeded());
+    // Assert default max_execution_time value does not exceed.
+    $this->executable->setMaxExecTime(30);
+    $this->assertFalse($this->executable->maxExecTimeExceeded());
+    // Assert max_execution_time value is exceeded.
+    $this->executable->setMaxExecTime(1);
+    $this->executable->setTimeElapsed(time() + 2);
+    $this->assertTrue($this->executable->maxExecTimeExceeded());
+  }
+
+  /**
+   * Tests the processRow method.
+   */
+  public function testProcessRow() {
+    $expected = array(
+      'test' => 'test destination',
+      'test1' => 'test1 destination'
+    );
+    foreach ($expected as $key => $value) {
+      $plugins[$key][0] = $this->getMock('Drupal\migrate\Plugin\MigrateProcessInterface');
+      $plugins[$key][0]->expects($this->once())
+        ->method('getPluginDefinition')
+        ->will($this->returnValue(array()));
+      $plugins[$key][0]->expects($this->once())
+        ->method('transform')
+        ->will($this->returnValue($value));
+    }
+    $this->migration->expects($this->once())
+      ->method('getProcessPlugins')
+      ->with(NULL)
+      ->will($this->returnValue($plugins));
+    $row = new Row(array(), array());
+    $this->executable->processRow($row);
+    foreach ($expected as $key => $value) {
+      $this->assertSame($row->getDestinationProperty($key), $value);
+    }
+    $this->assertSame(count($row->getDestination()), count($expected));
+  }
+
+  /**
+   * Returns a mock migration source instance.
+   *
+   * @return \Drupal\migrate\Source|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected function getMockSource() {
+    $iterator = $this->getMock('\Iterator');
+
+    $source = $this->getMockBuilder('Drupal\migrate\Source')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $source->expects($this->any())
+      ->method('getIterator')
+      ->will($this->returnValue($iterator));
+    $source->expects($this->once())
+      ->method('rewind')
+      ->will($this->returnValue(TRUE));
+    $source->expects($this->any())
+      ->method('valid')
+      ->will($this->onConsecutiveCalls(TRUE, FALSE));
+
+    return $source;
+  }
+
 }
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php
new file mode 100644
index 0000000..cde3037
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\MigrateExecuteableMemoryExceeded.
+ */
+
+namespace Drupal\migrate\Tests;
+
+/**
+ * Tests the MigrateExecuteable::memoryExceeded() method.
+ *
+ * @group Drupal
+ * @group migrate
+ */
+class MigrateExecuteableMemoryExceededTest extends MigrateTestCase {
+
+  /**
+   * The mocked migration entity.
+   *
+   * @var \Drupal\migrate\Entity\MigrationInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $migration;
+
+  /**
+   * The mocked migrate message.
+   *
+   * @var \Drupal\migrate\MigrateMessageInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $message;
+
+  /**
+   * The tested migrate executable.
+   *
+   * @var \Drupal\migrate\Tests\TestMigrateExecutable
+   */
+  protected $executable;
+
+  protected $mapJoinable = FALSE;
+
+  protected $migrationConfiguration = array(
+    'id' => 'test',
+  );
+
+  /**
+   * php.init memory_limit value.
+   */
+  protected $memoryLimit = 10000000;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Migrate executable memory exceeded',
+      'description' => 'Tests the migrate executable memoryExceeded method.',
+      'group' => 'Migrate',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->migration = $this->getMigration();
+    $this->message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
+
+    $this->executable = new TestMigrateExecutable($this->migration, $this->message);
+    $this->executable->setTranslationManager($this->getStringTranslationStub());
+  }
+
+  /**
+   * Runs the actual test.
+   *
+   * @param $message
+   *   The second message to assert.
+   * @param bool $memory_exceeded
+   *   Whether to test the memory exceeded case.
+   * @param int $memory_usage_first
+   * @param int $memory_usage_second
+   *   Used to set the second time the memory usage is queried: the first time
+   *   times this many will be returned.
+   */
+  protected function runMemoryExceededTest($message, $memory_exceeded, $memory_usage_first = NULL, $memory_usage_second = NULL, $memory_limit = NULL) {
+    $this->executable->setMemoryLimit($memory_limit ?: $this->memoryLimit);
+    $this->executable->setMemoryUsage($memory_usage_first ?: $this->memoryLimit, $memory_usage_second ?: $this->memoryLimit);
+    $this->migration->set('memoryThreshold', 0.85);
+    if ($message) {
+      $this->executable->message->expects($this->at(0))
+        ->method('display')
+        ->with($this->stringContains('reclaiming memory'));
+      $this->executable->message->expects($this->at(1))
+        ->method('display')
+        ->with($this->stringContains($message));
+    }
+    else {
+      $this->executable->message->expects($this->never())
+        ->method($this->anything());
+    }
+    $result = $this->executable->memoryExceeded();
+    $this->assertEquals($memory_exceeded, $result);
+  }
+
+  /**
+   * Tests memoryExceeded method when a new batch is needed.
+   */
+  public function testMemoryExceededNewBatch() {
+    // First case try reset and then start new batch.
+    $this->runMemoryExceededTest('starting new batch', TRUE);
+  }
+
+  /**
+   * Tests memoryExceeded method when enough is cleared.
+   */
+  public function testMemoryExceededClearedEnough() {
+    $this->runMemoryExceededTest('reclaimed enough', FALSE, $this->memoryLimit, $this->memoryLimit * 0.75);
+  }
+
+  /**
+   * Tests memoryExceeded when memory usage is not exceeded.
+   */
+  public function testMemoryNotExceeded() {
+    $this->runMemoryExceededTest('', FALSE, floor($this->memoryLimit * 0.85) - 1);
+  }
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php
new file mode 100644
index 0000000..7148654
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php
@@ -0,0 +1,192 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\MigrateSqlIdMapEnsureTablesTest.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+
+/**
+ * Test the Drupal\migrate\Plugin\migrate\id_map\Sql::ensureTables() method.
+ *
+ * @group Drupal
+ * @group migrate
+ */
+class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
+
+  protected $mapJoinable = FALSE;
+
+  protected $migrationConfiguration = array(
+    'id' => 'sql_idmap_test',
+    'sourceIds' => array(
+      'source_id_property' => array(
+        'type' => 'int',
+      ),
+    ),
+    'destinationIds' => array(
+      'destination_id_property' => array(
+        'type' => 'varchar',
+        'length' => 255,
+      ),
+    ),
+  );
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Sql::ensureTables()',
+      'description' => 'Tests the Sql id map plugin ensureTables() method.',
+      'group' => 'Migrate',
+    );
+  }
+
+  /**
+   * Test the ensureTables method.
+   */
+  public function testEnsureTablesNotExist() {
+    $fields['source_row_status'] = array(
+      'type' => 'int',
+      'size' => 'tiny',
+      'unsigned' => TRUE,
+      'not null' => TRUE,
+      'default' => MigrateIdMapInterface::STATUS_IMPORTED,
+      'description' => 'Indicates current status of the source row',
+    );
+    $fields['rollback_action'] = array(
+      'type' => 'int',
+      'size' => 'tiny',
+      'unsigned' => TRUE,
+      'not null' => TRUE,
+      'default' => MigrateIdMapInterface::ROLLBACK_DELETE,
+      'description' => 'Flag indicating what to do for this item on rollback',
+    );
+    $fields['last_imported'] = array(
+      'type' => 'int',
+      'unsigned' => TRUE,
+      'not null' => TRUE,
+      'default' => 0,
+      'description' => 'UNIX timestamp of the last time this row was imported',
+    );
+    $fields['hash'] = array(
+      'type' => 'varchar',
+      'length' => '32',
+      'not null' => FALSE,
+      'description' => 'Hash of source row data, for detecting changes',
+    );
+    $fields['sourceid1'] = $this->migrationConfiguration['sourceIds']['source_id_property'];
+    $fields['destid1'] = $this->migrationConfiguration['destinationIds']['destination_id_property'];
+    $fields['destid1']['not null'] = FALSE;
+    $map_table_schema = array(
+      'description' => 'Mappings from source identifier value(s) to destination identifier value(s).',
+      'fields' => $fields,
+      'primary key' => array('sourceid1'),
+    );
+    $schema = $this->getMockBuilder('Drupal\Core\Database\Schema')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $schema->expects($this->at(0))
+      ->method('tableExists')
+      ->with('migrate_map_sql_idmap_test')
+      ->will($this->returnValue(FALSE));
+    $schema->expects($this->at(1))
+      ->method('createTable')
+      ->with('migrate_map_sql_idmap_test', $map_table_schema);
+    // Now do the message table.
+    $fields = array();
+    $fields['msgid'] = array(
+      'type' => 'serial',
+      'unsigned' => TRUE,
+      'not null' => TRUE,
+    );
+    $fields['sourceid1'] = $this->migrationConfiguration['sourceIds']['source_id_property'];
+    $fields['level'] = array(
+      'type' => 'int',
+      'unsigned' => TRUE,
+      'not null' => TRUE,
+      'default' => 1,
+    );
+    $fields['message'] = array(
+      'type' => 'text',
+      'size' => 'medium',
+      'not null' => TRUE,
+    );
+    $table_schema = array(
+      'description' => 'Messages generated during a migration process',
+      'fields' => $fields,
+      'primary key' => array('msgid'),
+    );
+    $table_schema['indexes']['sourcekey'] = array('sourceid1');
+
+    $schema->expects($this->at(2))
+      ->method('createTable')
+      ->with('migrate_message_sql_idmap_test', $table_schema);
+    $schema->expects($this->exactly(3))
+      ->method($this->anything());
+    $this->runEnsureTablesTest($schema);
+  }
+
+  public function testEnsureTablesExist() {
+    $schema = $this->getMockBuilder('Drupal\Core\Database\Schema')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $schema->expects($this->at(0))
+      ->method('tableExists')
+      ->with('migrate_map_sql_idmap_test')
+      ->will($this->returnValue(TRUE));
+    $schema->expects($this->at(1))
+      ->method('fieldExists')
+      ->with('migrate_map_sql_idmap_test', 'rollback_action')
+      ->will($this->returnValue(FALSE));
+    $field_schema = array(
+      'type' => 'int',
+      'size' => 'tiny',
+      'unsigned' => TRUE,
+      'not null' => TRUE,
+      'default' => 0,
+      'description' => 'Flag indicating what to do for this item on rollback',
+    );
+    $schema->expects($this->at(2))
+      ->method('addField')
+      ->with('migrate_map_sql_idmap_test', 'rollback_action', $field_schema);
+    $schema->expects($this->at(3))
+      ->method('fieldExists')
+      ->with('migrate_map_sql_idmap_test', 'hash')
+      ->will($this->returnValue(FALSE));
+    $field_schema = array(
+      'type' => 'varchar',
+      'length' => '32',
+      'not null' => FALSE,
+      'description' => 'Hash of source row data, for detecting changes',
+    );
+    $schema->expects($this->at(4))
+      ->method('addField')
+      ->with('migrate_map_sql_idmap_test', 'hash', $field_schema);
+    $schema->expects($this->exactly(5))
+      ->method($this->anything());
+    $this->runEnsureTablesTest($schema);
+  }
+
+  /**
+   * Actually run the
+   *
+   * @param $schema
+   *   The mock schema object with expectations set. The Sql constructor calls
+   *   ensureTables() which in turn calls this object and the expectations on
+   *   it are the actual test and there are no additional asserts added.
+   */
+  protected function runEnsureTablesTest($schema) {
+    $database = $this->getMockBuilder('Drupal\Core\Database\Connection')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $database->expects($this->any())
+      ->method('schema')
+      ->will($this->returnValue($schema));
+    new TestSqlIdmap($database, array(), 'sql', array(), $this->getMigration());
+  }
+
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php
new file mode 100644
index 0000000..909779c
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php
@@ -0,0 +1,736 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\MigrateSqlIdMapTestCase.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\migrate\Row;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Entity\MigrationInterface;
+
+/**
+ * Tests the sql based ID map implementation.
+ *
+ * @group Drupal
+ * @group migrate
+ */
+class MigrateSqlIdMapTest extends MigrateTestCase {
+
+  protected $mapJoinable = FALSE;
+
+  protected $migrationConfiguration = array(
+    'id' => 'sql_idmap_test',
+    'sourceIds' => array(
+      'source_id_property' => array(),
+    ),
+    'destinationIds' => array(
+      'destination_id_property' => array(),
+    ),
+  );
+
+  /**
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Sql id map plugin',
+      'description' => 'Tests the Sql id map plugin.',
+      'group' => 'Migrate',
+    );
+  }
+
+  /**
+   * Creates a test sql id map plugin.
+   *
+   * @param array $database_contents
+   *   An array keyed by table names. Value are list of rows where each row is
+   *   an associative array of field => value.
+   * @param array $connection_options
+   *   An optional array of database connection options.
+   * @param array $prefix
+   *   Option default database prefix.
+   *
+   * @return \Drupal\migrate\Tests\TestSqlIdmap
+   *   A sql id map plugin test instance.
+   */
+  protected function getIdMap($database_contents = array(), $connection_options = array(), $prefix = '') {
+    $migration = $this->getMigration();
+    $this->database = $this->getDatabase($database_contents, $connection_options, $prefix);
+    $id_map = new TestSqlIdmap($this->database, array(), 'sql', array(), $migration);
+    $migration->expects($this->any())
+      ->method('getIdMap')
+      ->will($this->returnValue($id_map));
+    return $id_map;
+  }
+
+  /**
+   * Sets defaults for sql id map plugin tests.
+   */
+  protected function idMapDefaults() {
+    return array(
+      'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED,
+      'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
+      'hash' => '',
+    );
+  }
+
+  /**
+   * Tests the id mapping method.
+   *
+   * Create two id mappings and update the second to verify that:
+   *
+   * - saving new to empty tables work.
+   * - saving new to nonempty tables work.
+   * - updating work.
+   */
+  public function testSaveIdMapping() {
+    $source = array(
+      'source_id_property' => 'source_value',
+    );
+    $row = new Row($source, array('source_id_property' => array()));
+    $id_map = $this->getIdMap();
+    $id_map->saveIdMapping($row, array('destination_id_property' => 2));
+    $expected_result = array(
+      array(
+        'sourceid1' => 'source_value',
+        'destid1' => 2,
+      ) + $this->idMapDefaults(),
+    );
+    $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_result);
+    $source = array(
+      'source_id_property' => 'source_value_1',
+    );
+    $row = new Row($source, array('source_id_property' => array()));
+    $id_map->saveIdMapping($row, array('destination_id_property' => 3));
+    $expected_result[] = array(
+      'sourceid1' => 'source_value_1',
+      'destid1' => 3,
+    ) + $this->idMapDefaults();
+    $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_result);
+    $id_map->saveIdMapping($row, array('destination_id_property' => 4));
+    $expected_result[1]['destid1'] = 4;
+    $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_result);
+  }
+
+  /**
+   * Tests the sql id map set message method.
+   */
+  public function testSetMessage() {
+    $message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
+    $id_map = $this->getIdMap();
+    $id_map->setMessage($message);
+    $this->assertAttributeEquals($message, 'message', $id_map);
+  }
+
+  /**
+   * Test the clear messages method.
+   */
+  public function testClearMessages() {
+    $message = 'Hello world.';
+    $expected_results = array(0, 1, 2, 3);
+    $id_map = $this->getIdMap();
+
+    // Insert 4 message for later delete.
+    foreach ($expected_results as $key => $expected_result) {
+      $count = $id_map->messageCount();
+      $id_map->saveMessage(array($key), $message);
+    }
+
+    //Truncate and check that 4 messages were deleted.
+    $id_map->clearMessages();
+    $count = $id_map->messageCount();
+    $this->assertEquals($count, 0);
+  }
+
+  /**
+   * Tests the getRowsNeedingUpdate method for rows that need an update.
+   */
+  public function testGetRowsNeedingUpdate() {
+    $id_map = $this->getIdMap();
+    $row_statuses = array(
+      MigrateIdMapInterface::STATUS_IMPORTED,
+      MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
+      MigrateIdMapInterface::STATUS_IGNORED,
+      MigrateIdMapInterface::STATUS_FAILED,
+    );
+    // Create a mapping row for each STATUS constant.
+    foreach ($row_statuses as $status) {
+      $source = array('source_id_property' => 'source_value_' . $status);
+      $row = new Row($source, array('source_id_property' => array()));
+      $destination = array('destination_id_property' => 'destination_value_' . $status);
+      $id_map->saveIdMapping($row, $destination, $status);
+      $expected_results[] = array(
+        'sourceid1' => 'source_value_' . $status,
+        'destid1' => 'destination_value_' . $status,
+        'source_row_status' => $status,
+        'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
+        'hash' => '',
+      );
+      // Assert zero rows need an update.
+      if ($status == MigrateIdMapInterface::STATUS_IMPORTED) {
+        $rows_needing_update = $id_map->getRowsNeedingUpdate(1);
+        $this->assertCount(0, $rows_needing_update);
+      }
+    }
+    // Assert that test values exist.
+    $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results);
+
+    // Assert a single row needs an update.
+    $row_needing_update = $id_map->getRowsNeedingUpdate(1);
+    $this->assertCount(1, $row_needing_update);
+
+    // Assert the row matches its original source.
+    $source_id = $expected_results[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]['sourceid1'];
+    $test_row = $id_map->getRowBySource(array($source_id));
+    $this->assertSame($test_row, $row_needing_update[0]);
+
+    // Add additional row that needs an update.
+    $source = array('source_id_property' => 'source_value_multiple');
+    $row = new Row($source, array('source_id_property' => array()));
+    $destination = array('destination_id_property' => 'destination_value_multiple');
+    $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
+
+    // Assert multiple rows need an update.
+    $rows_needing_update = $id_map->getRowsNeedingUpdate(2);
+    $this->assertCount(2, $rows_needing_update);
+  }
+
+  /**
+   * Tests the sql id map message count method by counting and saving messages.
+   */
+  public function testMessageCount() {
+    $message = 'Hello world.';
+    $expected_results = array(0, 1, 2, 3);
+    $id_map = $this->getIdMap();
+
+    // Test count message multiple times starting from 0.
+    foreach ($expected_results as $key => $expected_result) {
+      $count = $id_map->messageCount();
+      $this->assertEquals($expected_result, $count);
+      $id_map->saveMessage(array($key), $message);
+    }
+  }
+
+  /**
+   * Tests the sql id map save message method.
+   */
+  public function testMessageSave() {
+    $message = 'Hello world.';
+    $expected_results = array(
+      1 => array('message' => $message, 'level' => MigrationInterface::MESSAGE_ERROR),
+      2 => array('message' => $message, 'level' => MigrationInterface::MESSAGE_WARNING),
+      3 => array('message' => $message, 'level' => MigrationInterface::MESSAGE_NOTICE),
+      4 => array('message' => $message, 'level' => MigrationInterface::MESSAGE_INFORMATIONAL),
+    );
+    $id_map = $this->getIdMap();
+
+    foreach ($expected_results as $key => $expected_result) {
+      $id_map->saveMessage(array($key), $message, $expected_result['level']);
+      $message_row = $this->database->select($id_map->getMessageTableName(), 'message')
+                       ->fields('message')
+                       ->condition('level',$expected_result['level'])
+                       ->condition('message',$expected_result['message'])
+                       ->execute()
+                       ->fetchAssoc();
+    $this->assertEquals($expected_result['message'], $message_row['message'], 'Message from database was read.');
+    }
+
+    // Insert with default level.
+    $message_default = 'Hello world default.';
+    $id_map->saveMessage(array(5), $message_default);
+    $message_row = $this->database->select($id_map->getMessageTableName(), 'message')
+                     ->fields('message')
+                     ->condition('level', MigrationInterface::MESSAGE_ERROR)
+                     ->condition('message', $message_default)
+                     ->execute()
+                     ->fetchAssoc();
+    $this->assertEquals($message_default, $message_row['message'], 'Message from database was read.');
+  }
+
+  /**
+   * Tests the getRowBySource method..
+   */
+  public function testGetRowBySource() {
+    $row = array(
+      'sourceid1' => 'source_id_value_1',
+      'sourceid2' => 'source_id_value_2',
+      'destid1' => 'destination_id_value_1',
+    ) + $this->idMapDefaults();
+    $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    $row = array(
+      'sourceid1' => 'source_id_value_3',
+      'sourceid2' => 'source_id_value_4',
+      'destid1' => 'destination_id_value_2',
+    ) + $this->idMapDefaults();
+    $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    $source_id_values = array($row['sourceid1'], $row['sourceid2']);
+    $id_map = $this->getIdMap($database_contents);
+    $result_row = $id_map->getRowBySource($source_id_values);
+    $this->assertSame($row, $result_row);
+    $source_id_values = array('missing_value_1', 'missing_value_2');
+    $result_row = $id_map->getRowBySource($source_id_values);
+    $this->assertFalse($result_row);
+  }
+
+  /**
+   * Tests the destination ID lookup method.
+   *
+   * Scenarios to test (for both hits and misses) are:
+   *
+   * - Single-value source ID to single-value destination ID.
+   * - Multi-value source ID to multi-value destination ID.
+   * - Single-value source ID to multi-value destination ID.
+   * - Multi-value source ID to single-value destination ID.
+   */
+  public function testLookupDestinationIDMapping() {
+    $this->performLookupDestinationIDTest(1, 1);
+    $this->performLookupDestinationIDTest(2, 2);
+    $this->performLookupDestinationIDTest(1, 2);
+    $this->performLookupDestinationIDTest(2, 1);
+  }
+
+  /**
+   * Performs destination ID test on source and destination fields.
+   *
+   * @param int $num_source_fields
+   *   Number of source fields to test.
+   * @param int $num_destination_fields
+   *   Number of destination fields to test.
+   */
+  protected function performLookupDestinationIDTest($num_source_fields, $num_destination_fields) {
+    // Adjust the migration configuration according to the number of source and
+    // destination fields.
+    $this->migrationConfiguration['sourceIds'] = array();
+    $this->migrationConfiguration['destinationIds'] = array();
+    $source_id_values = array();
+    $nonexistent_id_values = array();
+    $row = $this->idMapDefaults();
+    for ($i = 1; $i <= $num_source_fields; $i++) {
+      $row["sourceid$i"] = "source_id_value_$i";
+      $source_id_values[] = "source_id_value_$i";
+      $nonexistent_id_values[] = "nonexistent_source_id_value_$i";
+      $this->migrationConfiguration['sourceIds']["source_id_property_$i"] = array();
+    }
+    $expected_result = array();
+    for ($i = 1; $i <= $num_destination_fields; $i++) {
+      $row["destid$i"] = "destination_id_value_$i";
+      $expected_result[] = "destination_id_value_$i";
+      $this->migrationConfiguration['destinationIds']["destination_id_property_$i"] = array();
+    }
+    $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    $id_map = $this->getIdMap($database_contents);
+    // Test for a valid hit.
+    $destination_id = $id_map->lookupDestinationID($source_id_values);
+    $this->assertSame($expected_result, $destination_id);
+    // Test for a miss.
+    $destination_id = $id_map->lookupDestinationID($nonexistent_id_values);
+    $this->assertSame(0, count($destination_id));
+  }
+
+  /**
+   * Test the getRowByDestination method.
+   */
+  public function testGetRowByDestination() {
+    $row = array(
+      'sourceid1' => 'source_id_value_1',
+      'sourceid2' => 'source_id_value_2',
+      'destid1' => 'destination_id_value_1',
+    ) + $this->idMapDefaults();
+    $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    $row = array(
+      'sourceid1' => 'source_id_value_3',
+      'sourceid2' => 'source_id_value_4',
+      'destid1' => 'destination_id_value_2',
+    ) + $this->idMapDefaults();
+    $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    $dest_id_values = array($row['destid1']);
+    $id_map = $this->getIdMap($database_contents);
+    $result_row = $id_map->getRowByDestination($dest_id_values);
+    $this->assertSame($row, $result_row);
+    // This value does not exist.
+    $dest_id_values = array('invalid_destination_id_property');
+    $id_map = $this->getIdMap($database_contents);
+    $result_row = $id_map->getRowByDestination($dest_id_values);
+    $this->assertFalse($result_row);
+  }
+
+  /**
+   * Tests the source ID lookup method.
+   *
+   * Scenarios to test (for both hits and misses) are:
+   *
+   * - Single-value destination ID to single-value source ID.
+   * - Multi-value destination ID to multi-value source ID.
+   * - Single-value destination ID to multi-value source ID.
+   * - Multi-value destination ID to single-value source ID.
+   */
+  public function testLookupSourceIDMapping() {
+    $this->performLookupSourceIDTest(1, 1);
+    $this->performLookupSourceIDTest(2, 2);
+    $this->performLookupSourceIDTest(1, 2);
+    $this->performLookupSourceIDTest(2, 1);
+  }
+
+  /**
+   * Performs the source ID test on source and destination fields.
+   *
+   * @param int $num_source_fields
+   *   Number of source fields to test.
+   * @param int $num_destination_fields
+   *   Number of destination fields to test.
+   */
+  protected function performLookupSourceIDTest($num_source_fields, $num_destination_fields) {
+    // Adjust the migration configuration according to the number of source and
+    // destination fields.
+    $this->migrationConfiguration['sourceIds'] = array();
+    $this->migrationConfiguration['destinationIds'] = array();
+    $row = $this->idMapDefaults();
+    $expected_result = array();
+    for ($i = 1; $i <= $num_source_fields; $i++) {
+      $row["sourceid$i"] = "source_id_value_$i";
+      $expected_result[] = "source_id_value_$i";
+      $this->migrationConfiguration['sourceIds']["source_id_property_$i"] = array();
+    }
+    $destination_id_values = array();
+    $nonexistent_id_values = array();
+    for ($i = 1; $i <= $num_destination_fields; $i++) {
+      $row["destid$i"] = "destination_id_value_$i";
+      $destination_id_values[] = "destination_id_value_$i";
+      $nonexistent_id_values[] = "nonexistent_destination_id_value_$i";
+      $this->migrationConfiguration['destinationIds']["destination_id_property_$i"] = array();
+    }
+    $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    $id_map = $this->getIdMap($database_contents);
+    // Test for a valid hit.
+    $source_id = $id_map->lookupSourceID($destination_id_values);
+    $this->assertSame($expected_result, $source_id);
+    // Test for a miss.
+    $source_id = $id_map->lookupSourceID($nonexistent_id_values);
+    $this->assertSame(0, count($source_id));
+  }
+
+  /**
+   * Tests the imported count method.
+   *
+   * Scenarios to test for:
+   *
+   * - No imports.
+   * - One import.
+   * - Multiple imports.
+   */
+  public function testImportedCount() {
+    $id_map = $this->getIdMap();
+    // Add a single failed row and assert zero imported rows.
+    $source = array('source_id_property' => 'source_value_failed');
+    $row = new Row($source, array('source_id_property' => array()));
+    $destination = array('destination_id_property' => 'destination_value_failed');
+    $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_FAILED);
+    $imported_count = $id_map->importedCount();
+    $this->assertSame(0, $imported_count);
+
+    // Add an imported row and assert single count.
+    $source = array('source_id_property' => 'source_value_imported');
+    $row = new Row($source, array('source_id_property' => array()));
+    $destination = array('destination_id_property' => 'destination_value_imported');
+    $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_IMPORTED);
+    $imported_count = $id_map->importedCount();
+    $this->assertSame(1, $imported_count);
+
+    // Add a row needing update and assert multiple imported rows.
+    $source = array('source_id_property' => 'source_value_update');
+    $row = new Row($source, array('source_id_property' => array()));
+    $destination = array('destination_id_property' => 'destination_value_update');
+    $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
+    $imported_count = $id_map->importedCount();
+    $this->assertSame(2, $imported_count);
+  }
+
+  /**
+   * Tests the number of processed source rows.
+   *
+   * Scenarios to test for:
+   *
+   * - No processed rows.
+   * - One processed row.
+   * - Multiple processed rows.
+   */
+  public function testProcessedCount() {
+    $id_map = $this->getIdMap();
+    // Assert zero rows have been processed before adding rows.
+    $processed_count = $id_map->processedCount();
+    $this->assertSame(0, $processed_count);
+    $row_statuses = array(
+      MigrateIdMapInterface::STATUS_IMPORTED,
+      MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
+      MigrateIdMapInterface::STATUS_IGNORED,
+      MigrateIdMapInterface::STATUS_FAILED,
+    );
+    // Create a mapping row for each STATUS constant.
+    foreach ($row_statuses as $status) {
+      $source = array('source_id_property' => 'source_value_' . $status);
+      $row = new Row($source, array('source_id_property' => array()));
+      $destination = array('destination_id_property' => 'destination_value_' . $status);
+      $id_map->saveIdMapping($row, $destination, $status);
+      if ($status == MigrateIdMapInterface::STATUS_IMPORTED) {
+        // Assert a single row has been processed.
+        $processed_count = $id_map->processedCount();
+        $this->assertSame(1, $processed_count);
+      }
+    }
+    // Assert multiple rows have been processed.
+    $processed_count = $id_map->processedCount();
+    $this->assertSame(count($row_statuses), $processed_count);
+  }
+
+  /**
+   * Tests the update count method.
+   *
+   * Scenarios to test for:
+   *
+   * - No updates.
+   * - One update.
+   * - Multiple updates.
+   */
+  public function testUpdateCount() {
+    $this->performUpdateCountTest(0);
+    $this->performUpdateCountTest(1);
+    $this->performUpdateCountTest(3);
+  }
+
+  /**
+   * Performs the update count test with a given number of update rows.
+   *
+   * @param int $num_update_rows
+   *   The number of update rows to test.
+   */
+  protected function performUpdateCountTest($num_update_rows) {
+    $database_contents['migrate_map_sql_idmap_test'] = array();
+    for ($i = 0; $i < 5; $i++) {
+      $row = $this->idMapDefaults();
+      $row['sourceid1'] = "source_id_value_$i";
+      $row['destid1'] = "destination_id_value_$i";
+      $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
+      $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    }
+    for (; $i < 5 + $num_update_rows; $i++) {
+      $row = $this->idMapDefaults();
+      $row['sourceid1'] = "source_id_value_$i";
+      $row['destid1'] = "destination_id_value_$i";
+      $row['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
+      $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    }
+    $id_map = $this->getIdMap($database_contents);
+    $count = $id_map->updateCount();
+    $this->assertSame($num_update_rows, $count);
+  }
+
+  /**
+   * Tests the error count method.
+   *
+   * Scenarios to test for:
+   *
+   * - No errors.
+   * - One error.
+   * - Multiple errors.
+   */
+  public function testErrorCount() {
+    $this->performErrorCountTest(0);
+    $this->performErrorCountTest(1);
+    $this->performErrorCountTest(3);
+  }
+
+  /**
+   * Performs error count test with a given number of error rows.
+   *
+   * @param int $num_error_rows
+   *   Number of error rows to test.
+   */
+  protected function performErrorCountTest($num_error_rows) {
+    $database_contents['migrate_map_sql_idmap_test'] = array();
+    for ($i = 0; $i < 5; $i++) {
+      $row = $this->idMapDefaults();
+      $row['sourceid1'] = "source_id_value_$i";
+      $row['destid1'] = "destination_id_value_$i";
+      $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
+      $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    }
+    for (; $i < 5 + $num_error_rows; $i++) {
+      $row = $this->idMapDefaults();
+      $row['sourceid1'] = "source_id_value_$i";
+      $row['destid1'] = "destination_id_value_$i";
+      $row['source_row_status'] = MigrateIdMapInterface::STATUS_FAILED;
+      $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    }
+
+    $id_map = $this->getIdMap($database_contents);
+    $count = $id_map->errorCount();
+    $this->assertSame($num_error_rows, $count);
+  }
+
+  /**
+   * Tests setting a row source_row_status to STATUS_NEEDS_UPDATE.
+   */
+  public function testSetUpdate() {
+    $id_map = $this->getIdMap();
+    $row_statuses = array(
+      MigrateIdMapInterface::STATUS_IMPORTED,
+      MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
+      MigrateIdMapInterface::STATUS_IGNORED,
+      MigrateIdMapInterface::STATUS_FAILED,
+    );
+    // Create a mapping row for each STATUS constant.
+    foreach ($row_statuses as $status) {
+      $source = array('source_id_property' => 'source_value_' . $status);
+      $row = new Row($source, array('source_id_property' => array()));
+      $destination = array('destination_id_property' => 'destination_value_' . $status);
+      $id_map->saveIdMapping($row, $destination, $status);
+      $expected_results[] = array(
+        'sourceid1' => 'source_value_' . $status,
+        'destid1' => 'destination_value_' . $status,
+        'source_row_status' => $status,
+        'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
+        'hash' => '',
+      );
+    }
+    // Assert that test values exist.
+    $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results);
+    // Mark each row as STATUS_NEEDS_UPDATE.
+    foreach ($row_statuses as $status) {
+      $id_map->setUpdate(array('source_value_' . $status));
+    }
+    // Update expected results.
+    foreach ($expected_results as $key => $value) {
+      $expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
+    }
+    // Assert that updated expected values match.
+    $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results);
+    // Assert an exception is thrown when source identifiers are not provided.
+    try {
+      $id_map->setUpdate(array());
+      $this->assertFalse(FALSE, 'MigrateException not thrown, when source identifiers were provided to update.');
+    }
+    catch (MigrateException $e) {
+      $this->assertTrue(TRUE, "MigrateException thrown, when source identifiers were not provided to update.");
+    }
+  }
+
+  /**
+   * Test prepareUpdate().
+   */
+  public function testPrepareUpdate() {
+    $id_map = $this->getIdMap();
+    $row_statuses = array(
+      MigrateIdMapInterface::STATUS_IMPORTED,
+      MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
+      MigrateIdMapInterface::STATUS_IGNORED,
+      MigrateIdMapInterface::STATUS_FAILED,
+    );
+
+    // Create a mapping row for each STATUS constant.
+    foreach ($row_statuses as $status) {
+      $source = array('source_id_property' => 'source_value_' . $status);
+      $row = new Row($source, array('source_id_property' => array()));
+      $destination = array('destination_id_property' => 'destination_value_' . $status);
+      $id_map->saveIdMapping($row, $destination, $status);
+      $expected_results[] = array(
+        'sourceid1' => 'source_value_' . $status,
+        'destid1' => 'destination_value_' . $status,
+        'source_row_status' => $status,
+        'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
+        'hash' => '',
+      );
+    }
+
+    // Assert that test values exist.
+    $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results);
+
+    // Mark all rows as STATUS_NEEDS_UPDATE.
+    $id_map->prepareUpdate();
+
+    // Update expected results.
+    foreach ($expected_results as $key => $value) {
+      $expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
+    }
+    // Assert that updated expected values match.
+    $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results);
+  }
+
+  /**
+   * Tests the destroy method.
+   *
+   * Scenarios to test for:
+   *
+   * - No errors.
+   * - One error.
+   * - Multiple errors.
+   */
+  public function testDestroy() {
+    $id_map = $this->getIdMap();
+    $map_table_name = $id_map->getMapTableName();
+    $message_table_name = $id_map->getMessageTableName();
+    $row = new Row(array('source_id_property' => 'source_value'), array('source_id_property' => array()));
+    $id_map->saveIdMapping($row, array('destination_id_property' => 2));
+    $id_map->saveMessage(array('source_value'), 'A message');
+    $this->assertTrue($this->database->schema()->tableExists($map_table_name),
+                      "$map_table_name exists");
+    $this->assertTrue($this->database->schema()->tableExists($message_table_name),
+                      "$message_table_name exists");
+    $id_map->destroy();
+    $this->assertFalse($this->database->schema()->tableExists($map_table_name),
+                       "$map_table_name does not exist");
+    $this->assertFalse($this->database->schema()->tableExists($message_table_name),
+                       "$message_table_name does not exist");
+  }
+
+  /**
+   * Tests the getQualifiedMapTable method with an unprefixed database.
+   */
+  public function testGetQualifiedMapTableNoPrefix() {
+    $id_map = $this->getIdMap(array(), array('database' => 'source_database'));
+    $qualified_map_table = $id_map->getQualifiedMapTableName();
+    $this->assertEquals('source_database.migrate_map_sql_idmap_test', $qualified_map_table);
+  }
+
+  /**
+   * Tests the getQualifiedMapTable method with a prefixed database.
+   */
+  public function testGetQualifiedMapTablePrefix() {
+    $id_map = $this->getIdMap(array(), array('database' => 'source_database'), 'prefix');
+    $qualified_map_table = $id_map->getQualifiedMapTableName();
+    $this->assertEquals('migrate_map_sql_idmap_test', $qualified_map_table);
+  }
+
+  /**
+   * Test all the iterator methods in one swing.
+   *
+   * - Sql::rewind()
+   * - Sql::next()
+   * - Sql::valid()
+   * - Sql::key()
+   * - Sql::current()
+   */
+  public function testIterators() {
+    $database_contents['migrate_map_sql_idmap_test'] = array();
+    for ($i = 0; $i < 3; $i++) {
+      $row = $this->idMapDefaults();
+      $row['sourceid1'] = "source_id_value_$i";
+      $row['destid1'] = "destination_id_value_$i";
+      $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
+      $expected_results[serialize(array('sourceid1' => $row['sourceid1']))] = array('destid1' => $row['destid1']);
+      $database_contents['migrate_map_sql_idmap_test'][] = $row;
+    }
+
+    $id_map = $this->getIdMap($database_contents);
+    $this->assertSame(iterator_to_array($id_map), $expected_results);
+  }
+
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php
index 40d9f63..692cf2e 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php
@@ -30,9 +30,10 @@
    *   The mocked migration.
    */
   protected function getMigration() {
-    $idmap = $this->getMock('Drupal\migrate\Plugin\MigrateIdMapInterface');
+    $this->idMap = $this->getMock('Drupal\migrate\Plugin\MigrateIdMapInterface');
+
     if ($this->mapJoinable) {
-      $idmap->expects($this->once())
+      $this->idMap->expects($this->once())
         ->method('getQualifiedMapTableName')
         ->will($this->returnValue('test_map'));
     }
@@ -40,11 +41,14 @@ protected function getMigration() {
     $migration = $this->getMock('Drupal\migrate\Entity\MigrationInterface');
     $migration->expects($this->any())
       ->method('getIdMap')
-      ->will($this->returnValue($idmap));
-    $configuration = $this->migrationConfiguration;
-    $migration->expects($this->any())->method('get')->will($this->returnCallback(function ($argument) use ($configuration) {
+      ->will($this->returnValue($this->idMap));
+    $configuration = &$this->migrationConfiguration;
+    $migration->expects($this->any())->method('get')->will($this->returnCallback(function ($argument) use (&$configuration) {
       return isset($configuration[$argument]) ? $configuration[$argument] : '';
     }));
+    $migration->expects($this->any())->method('set')->will($this->returnCallback(function ($argument, $value) use (&$configuration) {
+      $configuration[$argument] = $value;
+    }));
     $migration->expects($this->any())
       ->method('id')
       ->will($this->returnValue($configuration['id']));
@@ -54,43 +58,8 @@ protected function getMigration() {
   /**
    * @return \Drupal\Core\Database\Connection
    */
-  protected function getDatabase($database_contents) {
-    $database = $this->getMockBuilder('Drupal\Core\Database\Connection')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $database->databaseContents = &$database_contents;
-
-    // Although select doesn't modify the contents of the database, it still
-    // needs to be a reference so that we can select previously inserted or
-    // updated rows.
-    $database->expects($this->any())
-      ->method('select')->will($this->returnCallback(function ($base_table, $base_alias) use (&$database_contents) {
-      return new FakeSelect($base_table, $base_alias, $database_contents);
-    }));
-    $database->expects($this->any())
-      ->method('schema')
-      ->will($this->returnCallback(function () use (&$database_contents) {
-      return new FakeDatabaseSchema($database_contents);
-    }));
-    $database->expects($this->any())
-      ->method('insert')
-      ->will($this->returnCallback(function ($table) use (&$database_contents) {
-      return new FakeInsert($database_contents, $table);
-    }));
-    $database->expects($this->any())
-      ->method('update')
-      ->will($this->returnCallback(function ($table) use (&$database_contents) {
-      return new FakeUpdate($database_contents, $table);
-    }));
-    $database->expects($this->any())
-      ->method('merge')
-      ->will($this->returnCallback(function ($table) use (&$database_contents) {
-      return new FakeMerge($database_contents, $table);
-    }));
-    $database->expects($this->any())
-      ->method('query')
-      ->will($this->throwException(new \Exception('Query is not supported')));
-    return $database;
+  protected function getDatabase($database_contents, $connection_options = array(), $prefix = '') {
+    return new FakeConnection($database_contents, $connection_options, $prefix);
   }
 
   /**
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php
new file mode 100644
index 0000000..f900651
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php
@@ -0,0 +1,238 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\RowTest.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Row;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests for Row class.
+ *
+ * @group Drupal
+ * @group migrate
+ *
+ * @covers \Drupal\migrate\Row
+ */
+class RowTest extends UnitTestCase {
+
+  protected $testSourceIds = array(
+    'nid' => 'Node ID',
+  );
+  protected $testValues = array(
+    'nid' => 1,
+    'title' => 'node 1',
+  );
+  protected $testHash = '85795d4cde4a2425868b812cc88052ecd14fc912e7b9b4de45780f66750e8b1e';
+  // After changing title value to 'new title'.
+  protected $testHashMod = '9476aab0b62b3f47342cc6530441432e5612dcba7ca84115bbab5cceaca1ecb3';
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Row class functionality',
+      'description' => 'Tests Row class functionality.',
+      'group' => 'Migrate',
+    );
+  }
+
+  /**
+   * Tests object creation: empty.
+   */
+  public function testRowWithoutData() {
+    $row = new Row(array(), array());
+    $this->assertSame(array(), $row->getSource(), 'Empty row');
+  }
+
+  /**
+   * Tests object creation: basic.
+   */
+  public function testRowWithBasicData() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    $this->assertSame($this->testValues, $row->getSource(), 'Row with data, simple id.');
+  }
+
+  /**
+   * Tests object creation: multiple source IDs.
+   */
+  public function testRowWithMultipleSourceIds() {
+    $multi_source_ids = $this->testSourceIds + array('vid' => 'Node revision');
+    $multi_source_ids_values = $this->testValues + array('vid' => 1);
+    $row = new Row($multi_source_ids_values, $multi_source_ids);
+    $this->assertSame($multi_source_ids_values, $row->getSource(), 'Row with data, multifield id.');
+  }
+
+  /**
+   * Tests object creation: invalid values.
+   *
+   * @expectedException \Exception
+   */
+  public function testRowWithInvalidData() {
+    $invalid_values = array(
+      'title' => 'node X',
+    );
+    $row = new Row($invalid_values, $this->testSourceIds);
+  }
+
+  /**
+   * Tests source immutability after freeze.
+   *
+   * @expectedException \Exception
+   */
+  public function testSourceFreeze() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    $row->rehash();
+    $this->assertSame($this->testHash, $row->getHash(), 'Correct hash.');
+    $row->setSourceProperty('title', 'new title');
+    $row->rehash();
+    $this->assertSame($this->testHashMod, $row->getHash(), 'Hash changed correctly.');
+    $row->freezeSource();
+    $row->setSourceProperty('title', 'new title');
+  }
+
+  /**
+   * Tests setting on a frozen row.
+   *
+   * @expectedException \Exception
+   * @expectedExceptionMessage The source is frozen and can't be changed any more
+   */
+  public function testSetFrozenRow() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    $row->freezeSource();
+    $row->setSourceProperty('title', 'new title');
+  }
+
+  /**
+   * Tests hashing.
+   */
+  public function testHashing() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    $this->assertSame('', $row->getHash(), 'No hash at creation');
+    $row->rehash();
+    $this->assertSame($this->testHash, $row->getHash(), 'Correct hash.');
+    $row->rehash();
+    $this->assertSame($this->testHash, $row->getHash(), 'Correct hash even doing it twice.');
+
+    // Set the map to needs update.
+    $test_id_map = array(
+      'original_hash' => '',
+      'hash' => '',
+      'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
+    );
+    $row->setIdMap($test_id_map);
+    $this->assertTrue($row->needsUpdate());
+
+    $row->rehash();
+    $this->assertSame($this->testHash, $row->getHash(), 'Correct hash even if id_mpa have changed.');
+    $row->setSourceProperty('title', 'new title');
+    $row->rehash();
+    $this->assertSame($this->testHashMod, $row->getHash(), 'Hash changed correctly.');
+
+    // Set the map to successfully imported.
+    $test_id_map = array(
+      'original_hash' => '',
+      'hash' => '',
+      'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED,
+    );
+    $row->setIdMap($test_id_map);
+    $this->assertFalse($row->needsUpdate());
+
+    // Set the same hash value and ensure it was not changed.
+    $random = $this->randomName();
+    $test_id_map = array(
+      'original_hash' => $random,
+      'hash' => $random,
+      'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
+    );
+    $row->setIdMap($test_id_map);
+    $this->assertFalse($row->changed());
+
+    // Set different has values to ensure it is marked as changed.
+    $test_id_map = array(
+      'original_hash' => $this->randomName(),
+      'hash' => $this->randomName(),
+      'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
+    );
+    $row->setIdMap($test_id_map);
+    $this->assertTrue($row->changed());
+  }
+
+  /**
+   * Tests getting/setting the idMap.
+   *
+   * @covers \Drupal\migrate\Row::setIdMap()
+   * @covers \Drupal\migrate\Row::getIdMap()
+   */
+  public function testGetSetIdMap() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    $test_id_map = array(
+      'original_hash' => '',
+      'hash' => '',
+      'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
+    );
+    $row->setIdMap($test_id_map);
+    $this->assertEquals($test_id_map, $row->getIdMap());
+  }
+
+  /**
+   * Tests the source ID.
+   */
+  public function testSourceIdValues() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    $this->assertSame(array('nid' => $this->testValues['nid']), $row->getSourceIdValues());
+  }
+
+  /**
+   * Tests getting the source property.
+   *
+   * @covers \Drupal\migrate\Row::getSourceProperty()
+   */
+  public function testGetSourceProperty() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    $this->assertSame($this->testValues['nid'], $row->getSourceProperty('nid'));
+    $this->assertSame($this->testValues['title'], $row->getSourceProperty('title'));
+    $this->assertNull($row->getSourceProperty('non_existing'));
+  }
+
+  /**
+   * Tests setting and getting the destination.
+   */
+  public function testDestination() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    $this->assertEmpty($row->getDestination());
+    $this->assertFalse($row->hasDestinationProperty('nid'));
+
+    // Set a destination.
+    $row->setDestinationProperty('nid', 2);
+    $this->assertTrue($row->hasDestinationProperty('nid'));
+    $this->assertEquals(array('nid' => 2), $row->getDestination());
+  }
+
+  /**
+   * Tests setting/getting multiple destination IDs.
+   */
+  public function testMultipleDestination() {
+    $row = new Row($this->testValues, $this->testSourceIds);
+    // Set some deep nested values.
+    $row->setDestinationProperty('image:alt', 'alt text');
+    $row->setDestinationProperty('image:fid', 3);
+
+    $this->assertTrue($row->hasDestinationProperty('image'));
+    $this->assertFalse($row->hasDestinationProperty('alt'));
+    $this->assertFalse($row->hasDestinationProperty('fid'));
+
+    $destination = $row->getDestination();
+    $this->assertEquals('alt text', $destination['image']['alt']);
+    $this->assertEquals(3, $destination['image']['fid']);
+    $this->assertEquals('alt text', $row->getDestinationProperty('image:alt'));
+    $this->assertEquals(3, $row->getDestinationProperty('image:fid'));
+  }
+
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php b/core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php
new file mode 100644
index 0000000..46090fa
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\TestMigrateExecutable.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\migrate\MigrateExecutable;
+
+class TestMigrateExecutable extends MigrateExecutable {
+
+
+  /**
+   * (Fake) number of seconds elapsed since the start of the test.
+   *
+   * @var int
+   */
+  protected $timeElapsed;
+
+  /**
+   * Fake memory usage in bytes.
+   *
+   * @var int
+   */
+  protected $memoryUsage;
+
+  /**
+   * The cleared memory usage.
+   */
+  protected $clearedMemoryUsage;
+
+  public function setTranslationManager(TranslationInterface $translation_manager) {
+    $this->translationManager = $translation_manager;
+  }
+
+  /**
+   * Allows access to protected timeOptionExceeded method.
+   */
+  public function timeOptionExceeded() {
+    return parent::timeOptionExceeded();
+  }
+
+  /**
+   * Allows access to set protected maxExecTime property.
+   */
+  public function setMaxExecTime($max_exec_time) {
+    $this->maxExecTime = $max_exec_time;
+  }
+
+  /**
+   * Allows access to protected maxExecTime property.
+   */
+  public function getMaxExecTime() {
+    return $this->maxExecTime;
+  }
+
+  public function getSuccessesSinceFeedback() {
+    return $this->successesSinceFeedback;
+  }
+
+  public function getTotalSuccesses() {
+    return $this->totalSuccesses;
+  }
+
+  public function getTotalProcessed() {
+    return $this->totalProcessed;
+  }
+
+  public function getProcessedSinceFeedback() {
+    return $this->processedSinceFeedback;
+  }
+
+  /**
+   * Allows access to protected maxExecTimeExceeded method.
+   */
+  public function maxExecTimeExceeded() {
+    return parent::maxExecTimeExceeded();
+  }
+
+  /**
+   *
+   */
+  public function setSource($source) {
+    $this->source = $source;
+  }
+
+  /**
+   * Allows access to protected sourceIdValues property.
+   */
+  public function setSourceIdValues($source_id_values) {
+    $this->sourceIdValues = $source_id_values;
+  }
+
+  /**
+   * Allows setting a fake elapsed time.
+   */
+  public function setTimeElapsed($time) {
+    $this->timeElapsed = $time;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTimeElapsed() {
+    return $this->timeElapsed;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handleException($exception, $save = TRUE) {
+    $message = $exception->getMessage();
+    if ($save) {
+      $this->saveMessage($message);
+    }
+    $this->message->display($message);
+  }
+
+  /**
+   * Allows access to the protected memoryExceeded method.
+   *
+   * @return bool
+   */
+  public function memoryExceeded() {
+    return parent::memoryExceeded();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function attemptMemoryReclaim() {
+    return $this->clearedMemoryUsage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getMemoryUsage() {
+    return $this->memoryUsage;
+  }
+
+  /**
+   * Set the fake memory usage.
+   */
+  public function setMemoryUsage($memory_usage, $cleared_memory_usage = NULL) {
+    $this->memoryUsage = $memory_usage;
+    $this->clearedMemoryUsage = $cleared_memory_usage;
+  }
+
+  /**
+   * Set the memory limit.
+   */
+  public function setMemoryLimit($memory_limit) {
+    $this->memoryLimit = $memory_limit;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatSize($size) {
+    return $size;
+  }
+
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdmap.php b/core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdmap.php
new file mode 100644
index 0000000..cedddc9
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdmap.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\TestSqlIdmap.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\migrate\Entity\MigrationInterface;
+use Drupal\migrate\Plugin\migrate\id_map\Sql;
+
+class TestSqlIdmap extends Sql {
+
+  function __construct($database, array $configuration, $plugin_id, array $plugin_definition, MigrationInterface $migration) {
+    $this->database = $database;
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
+  }
+
+  public function getDatabase() {
+    return parent::getDatabase();
+  }
+}
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/process/DedupeEntityTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/process/DedupeEntityTest.php
index e4ccc75..656753e 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/process/DedupeEntityTest.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/DedupeEntityTest.php
@@ -44,65 +44,43 @@ public function setUp() {
   }
 
   /**
-   * Tests the entity deduplication plugin when there is no duplication.
+   * Tests entity based deduplication based on providerTestDedupe() values.
+   *
+   * @dataProvider providerTestDedupe
    */
-  public function testDedupeEntityNoDuplication() {
+  public function testDedupe($count, $postfix = '') {
     $configuration = array(
       'entity_type' => 'test_entity_type',
       'field' => 'test_field',
     );
+    if ($postfix) {
+      $configuration['postfix'] = $postfix;
+    }
     $plugin = new TestDedupeEntity($configuration, 'dedupe_entity', array());
-    $this->entityQueryExpects(0);
+    $this->entityQueryExpects($count);
     $plugin->setEntityQuery($this->entityQuery);
     $return = $plugin->transform('test', $this->migrateExecutable, $this->row, 'testpropertty');
-    $this->assertSame($return, 'test');
+    $this->assertSame($return, 'test' . ($count ? $postfix . $count : ''));
   }
 
   /**
-   * Tests the entity deduplication plugin when there is duplication.
+   * Data provider for testDedupe().
    */
-  public function testDedupeEntityDuplication() {
-    $configuration = array(
-      'entity_type' => 'test_entity_type',
-      'field' => 'test_field',
+  public function providerTestDedupe() {
+    return array(
+      // Tests the entity deduplication plugin when there is no duplication
+      // and no postfix.
+      array(0),
+      // Tests the entity deduplication plugin when there is duplication but
+      // no postfix.
+      array(3),
+      // Tests the entity deduplication plugin when there is no duplication
+      // but there is a postfix.
+      array(0, '_'),
+      // Tests the entity deduplication plugin when there is duplication and
+      // there is a postfix.
+      array(2, '_'),
     );
-    $plugin = new TestDedupeEntity($configuration, 'dedupe_entity', array());
-    $this->entityQueryExpects(3);
-    $plugin->setEntityQuery($this->entityQuery);
-    $return = $plugin->transform('test', $this->migrateExecutable, $this->row, 'testpropertty');
-    $this->assertSame($return, 'test3');
-  }
-
-  /**
-   * Tests the entity deduplication plugin when there is no duplication.
-   */
-  public function testDedupeEntityNoDuplicationWithPostfix() {
-    $configuration = array(
-      'entity_type' => 'test_entity_type',
-      'field' => 'test_field',
-      'postfix' => '_',
-    );
-    $plugin = new TestDedupeEntity($configuration, 'dedupe_entity', array());
-    $this->entityQueryExpects(0);
-    $plugin->setEntityQuery($this->entityQuery);
-    $return = $plugin->transform('test', $this->migrateExecutable, $this->row, 'testpropertty');
-    $this->assertSame($return, 'test');
-  }
-
-  /**
-   * Tests the entity deduplication plugin when there is duplication.
-   */
-  public function testDedupeEntityDuplicationWithPostfix() {
-    $configuration = array(
-      'entity_type' => 'test_entity_type',
-      'field' => 'test_field',
-      'postfix' => '_',
-    );
-    $plugin = new TestDedupeEntity($configuration, 'dedupe_entity', array());
-    $this->entityQueryExpects(2);
-    $plugin->setEntityQuery($this->entityQuery);
-    $return = $plugin->transform('test', $this->migrateExecutable, $this->row, 'testpropertty');
-    $this->assertSame($return, 'test_2');
   }
 
   /**
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php
index e49e1f2..60e66a7 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php
@@ -25,11 +25,17 @@ public static function getInfo() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
   function setUp() {
     $this->plugin = new TestGet();
     parent::setUp();
   }
 
+  /**
+   * Tests the Get plugin when source is a string.
+   */
   function testTransformSourceString() {
     $this->row->expects($this->once())
       ->method('getSourceProperty')
@@ -40,6 +46,9 @@ function testTransformSourceString() {
     $this->assertSame($value, 'source_value');
   }
 
+  /**
+   * Tests the Get plugin when source is an array.
+   */
   function testTransformSourceArray() {
     $map = array(
       'test1' => 'source_value1',
@@ -53,6 +62,9 @@ function testTransformSourceArray() {
     $this->assertSame($value, array('source_value1', 'source_value2'));
   }
 
+  /**
+   * Tests the Get plugin when source is a string pointing to destination.
+   */
   function testTransformSourceStringAt() {
     $this->row->expects($this->once())
       ->method('getSourceProperty')
@@ -63,6 +75,9 @@ function testTransformSourceStringAt() {
     $this->assertSame($value, 'source_value');
   }
 
+  /**
+   * Tests the Get plugin when source is an array pointing to destination.
+   */
   function testTransformSourceArrayAt() {
     $map = array(
       'test1' => 'source_value1',
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/process/IteratorTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/process/IteratorTest.php
index 39aeb54..454206f 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/process/IteratorTest.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/IteratorTest.php
@@ -9,6 +9,7 @@
 use Drupal\migrate\MigrateExecutable;
 use Drupal\migrate\Plugin\migrate\process\Get;
 use Drupal\migrate\Plugin\migrate\process\Iterator;
+use Drupal\migrate\Plugin\migrate\process\StaticMap;
 use Drupal\migrate\Row;
 use Drupal\migrate\Tests\MigrateTestCase;
 
@@ -53,7 +54,7 @@ public static function getInfo() {
   /**
    * Tests the iterator process plugin.
    */
-  function testIterator() {
+  public function testIterator() {
     $migration = $this->getMigration();
     // Set up the properties for the iterator.
     $configuration = array(
diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php b/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php
index d99a865..923ca97 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php
@@ -12,7 +12,7 @@
 abstract class MigrateProcessTestCase extends MigrateTestCase {
 
   /**
-   * @var \Drupal\migrate\Plugin\migrate\process\TestGet
+   * @var \Drupal\migrate\Plugin\MigrateProcessInterface
    */
   protected $plugin;
 
