From 3f369f179b9f50ef1490c8e4ff8cc35938ef8f17 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Thu, 9 Jan 2014 14:19:06 +0000
Subject: [PATCH] Issue #2160345 by chx, YesCT, andypost: Clean up and test
 migrate executable and sql idmap.

---
 .../lib/Drupal/migrate/Entity/Migration.php   |  27 +
 .../migrate/Entity/MigrationInterface.php     |   5 +-
 .../lib/Drupal/migrate/MigrateExecutable.php  | 356 +++++----
 .../migrate/MigrateSkipRowException.php       |  15 +
 .../migrate/Plugin/MigrateIdMapInterface.php  |   2 +-
 .../migrate/Plugin/migrate/id_map/Sql.php     |  26 +-
 .../Drupal/migrate/Tests/FakeConnection.php   | 160 ++++
 .../migrate/Tests/FakeDatabaseSchema.php      |  14 +-
 .../tests/Drupal/migrate/Tests/FakeInsert.php |  71 ++
 .../tests/Drupal/migrate/Tests/FakeMerge.php  |  61 ++
 .../tests/Drupal/migrate/Tests/FakeSelect.php |  11 +-
 .../Drupal/migrate/Tests/FakeStatement.php    |  15 +-
 .../Drupal/migrate/Tests/FakeTruncate.php     |  36 +
 .../tests/Drupal/migrate/Tests/FakeUpdate.php |  88 +++
 .../migrate/Tests/MigrateExecutableTest.php   | 389 ++++++++-
 .../MigrateExecuteableMemoryExceededTest.php  | 139 ++++
 .../Tests/MigrateSqlIdMapEnsureTablesTest.php | 205 +++++
 .../migrate/Tests/MigrateSqlIdMapTest.php     | 741 ++++++++++++++++++
 .../Drupal/migrate/Tests/MigrateTestCase.php  |  66 +-
 .../tests/Drupal/migrate/Tests/RowTest.php    | 260 ++++++
 .../migrate/Tests/TestMigrateExecutable.php   | 231 ++++++
 .../Drupal/migrate/Tests/TestSqlIdMap.php     |  45 ++
 .../Drupal/migrate/Tests/process/GetTest.php  |  15 +
 .../migrate/Tests/process/IteratorTest.php    |   3 +-
 .../Tests/process/MigrateProcessTestCase.php  |   2 +-
 .../Tests/d6/MigrateUserRoleTest.php          |   4 +-
 26 files changed, 2742 insertions(+), 245 deletions(-)
 create mode 100644 core/modules/migrate/lib/Drupal/migrate/MigrateSkipRowException.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php
 create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdMap.php

diff --git a/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php
index 60df0686a45a..e572bed8d06b 100644
--- a/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php
+++ b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php
@@ -174,6 +174,16 @@ class Migration extends ConfigEntityBase implements MigrationInterface {
    */
   public $sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
 
+  /**
+   * The ratio 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 0.85.
+   *
+   * @var float
+   */
+  protected $memoryThreshold = 0.85;
+
   /**
    * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
    */
@@ -184,6 +194,23 @@ class Migration extends ConfigEntityBase implements MigrationInterface {
    */
   public $trackLastImported = FALSE;
 
+  /**
+   * The ratio 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 0.9.
+   *
+   * @var float
+   */
+  public $timeThreshold = 0.90;
+
+  /**
+   * The time limit when executing the migration.
+   *
+   * @var array
+   */
+  public $limit = array();
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php b/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php
index 1fa3dad0ee7b..4999cdf87c9a 100644
--- a/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php
+++ b/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php
@@ -63,8 +63,9 @@ public function getSourcePlugin();
    * @param array $process
    *   A process configuration array.
    *
-   * @return array
-   *   A list of process plugins.
+   * @return \Drupal\migrate\Plugin\MigrateProcessInterface[][]
+   *   An associative array. The keys are the destination property names. Values
+   *   are process pipelines. Each pipeline contains an array of plugins.
    */
   public function getProcessPlugins(array $process = NULL);
 
diff --git a/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php
index 7d78cd419fad..104f187b377d 100644
--- a/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php
+++ b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php
@@ -28,14 +28,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.
@@ -54,7 +54,7 @@ class MigrateExecutable {
    *
    * @var int
    */
-  protected $total_processed;
+  protected $totalProcessed;
 
   /**
    * The queued messages not yet saved.
@@ -78,14 +78,25 @@ class MigrateExecutable {
   protected $options;
 
   /**
-   * The fraction of the memory limit at which an operation will be interrupted.
+   * The PHP max_execution_time.
    *
-   * Can be overridden by a Migration subclass if one would like to push the
-   * envelope. Defaults to 85%.
+   * @var int
+   */
+  protected $maxExecTime;
+
+  /**
+   * The configuration values of the source.
    *
-   * @var float
+   * @var array
    */
-  protected $memoryThreshold = 0.85;
+  protected $sourceIdValues;
+
+  /**
+   * The number of rows processed since feedback was given.
+   *
+   * @var int
+   */
+  protected $processedSinceFeedback = 0;
 
   /**
    * The PHP memory_limit expressed in bytes.
@@ -95,42 +106,55 @@ class MigrateExecutable {
   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%.
+   * The translation manager.
    *
-   * @var float
+   * @var \Drupal\Core\StringTranslation\TranslationInterface
    */
-  protected $timeThreshold = 0.90;
+  protected $translationManager;
 
   /**
-   * The PHP max_execution_time.
+   * The rollback action to be saved for the current row.
    *
    * @var int
    */
-  protected $timeLimit;
+  public $rollbackAction;
 
   /**
-   * The configuration values of the source.
+   * An array of counts. Initially used for cache hit/miss tracking.
    *
    * @var array
    */
-  protected $sourceIdValues;
+  protected $counts = array();
 
   /**
-   * The number of rows processed since feedback was given.
+   * The maximum number of items to pass in a single call during a rollback.
+   *
+   * For use in bulkRollback(). Can be overridden in derived class constructor.
    *
    * @var int
    */
-  protected $processed_since_feedback = 0;
+  protected $rollbackBatchSize = 50;
 
   /**
-   * The translation manager.
+   * The object currently being constructed.
    *
-   * @var \Drupal\Core\StringTranslation\TranslationInterface
+   * @var \stdClass
    */
-  protected $translationManager;
+  protected $destinationValues;
+
+  /**
+   * The source.
+   *
+   * @var \Drupal\migrate\Source
+   */
+  protected $source;
+
+  /**
+   * The current data row retrieved from the source.
+   *
+   * @var \stdClass
+   */
+  protected $sourceValues;
 
   /**
    * Constructs a MigrateExecutable and verifies and sets the memory limit.
@@ -169,6 +193,8 @@ public function __construct(MigrationInterface $migration, MigrateMessageInterfa
       }
       $this->memoryLimit = $limit;
     }
+    // Record the maximum execution time limit.
+    $this->maxExecTime = ini_get('max_execution_time');
   }
 
   /**
@@ -187,41 +213,7 @@ public function getSource() {
   }
 
   /**
-   * The rollback action to be saved for the current row.
-   *
-   * @var int
-   */
-  public $rollbackAction;
-
-  /**
-   * An array of counts. Initially used for cache hit/miss tracking.
-   *
-   * @var array
-   */
-  protected $counts = array();
-
-  /**
-   * When performing a bulkRollback(), the maximum number of items to pass in
-   * a single call. Can be overridden in derived class constructor.
-   *
-   * @var int
-   */
-  protected $rollbackBatchSize = 50;
-
-  /**
-   * The object currently being constructed
-   * @var \stdClass
-   */
-  protected $destinationValues;
-
-  /**
-   * The current data row retrieved from the source.
-   * @var \stdClass
-   */
-  protected $sourceValues;
-
-  /**
-   * Perform an import operation - migrate items from source to destination.
+   * Performs an import operation - migrate items from source to destination.
    */
   public function import() {
     $return = MigrationInterface::RESULT_COMPLETED;
@@ -238,44 +230,55 @@ 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();
       }
 
-      $this->processRow($row);
-
       try {
-        $destination_id_values = $destination->import($row);
-        // @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++;
-        }
-        else {
-          $id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction);
-          if ($id_map->messageCount() == 0) {
-            $message = $this->t('New object was not saved, no error provided');
-            $this->saveMessage($message);
-            $this->message->display($message);
-          }
-        }
+        $this->processRow($row);
+        $save = TRUE;
       }
-      catch (MigrateException $e) {
-        $this->migration->getIdMap()->saveIdMapping($row, array(), $e->getStatus(), $this->rollbackAction);
-        $this->saveMessage($e->getMessage(), $e->getLevel());
-        $this->message->display($e->getMessage());
+      catch (MigrateSkipRowException $e) {
+        $id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_IGNORED, $this->rollbackAction);
+        $save = FALSE;
       }
-      catch (\Exception $e) {
-        $this->migration->getIdMap()->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction);
-        $this->handleException($e);
+
+      if ($save) {
+        try {
+          $destination_id_values = $destination->import($row);
+          // @todo Handle the successful but no ID case like config,
+          //   https://drupal.org/node/2160835.
+          if ($destination_id_values) {
+            $id_map->saveIdMapping($row, $destination_id_values, $this->sourceRowStatus, $this->rollbackAction);
+            $this->successesSinceFeedback++;
+            $this->totalSuccesses++;
+          }
+          else {
+            $id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction);
+            if (!$id_map->messageCount()) {
+              $message = $this->t('New object was not saved, no error provided');
+              $this->saveMessage($message);
+              $this->message->display($message);
+            }
+          }
+        }
+        catch (MigrateException $e) {
+          $this->migration->getIdMap()->saveIdMapping($row, array(), $e->getStatus(), $this->rollbackAction);
+          $this->saveMessage($e->getMessage(), $e->getLevel());
+          $this->message->display($e->getMessage());
+        }
+        catch (\Exception $e) {
+          $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']));
       }
@@ -284,12 +287,6 @@ public function import() {
       unset($sourceValues, $destinationValues);
       $this->sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
 
-      // TODO: Temporary. Remove when http://drupal.org/node/375494 is committed.
-      // TODO: Should be done in MigrateDestinationEntity
-      if (!empty($destination->entityType)) {
-        entity_get_controller($destination->entityType)->resetCache();
-      }
-
       if (($return = $this->checkStatus()) != MigrationInterface::RESULT_COMPLETED) {
         break;
       }
@@ -316,22 +313,27 @@ public function import() {
   }
 
   /**
-   * @param Row $row
+   * Processes a row.
+   *
+   * @param \Drupal\migrate\Row $row
    *   The $row to be processed.
    * @param array $process
-   *   A process pipeline configuration. If not set, the top level process
-   *   configuration in the migration entity is used.
+   *   (optional) A process pipeline configuration. If not set, the top level
+   *   process configuration in the migration entity is used.
    * @param mixed $value
-   *   Optional initial value of the pipeline for the first destination.
+   *   (optional) Initial value of the pipeline for the first destination.
    *   Usually setting this is not necessary as $process typically starts with
    *   a 'get'. This is useful only when the $process contains a single
    *   destination and needs to access a value outside of the source. See
    *   \Drupal\migrate\Plugin\migrate\process\Iterator::transformKey for an
    *   example.
+   *
+   * @throws \Drupal\migrate\MigrateException
    */
   public function processRow(Row $row, array $process = NULL, $value = NULL) {
     foreach ($this->migration->getProcessPlugins($process) as $destination => $plugins) {
       $multiple = FALSE;
+      /** @var $plugin \Drupal\migrate\Plugin\MigrateProcessInterface */
       foreach ($plugins as $plugin) {
         $definition = $plugin->getPluginDefinition();
         // Many plugins expect a scalar value but the current value of the
@@ -353,32 +355,38 @@ public function processRow(Row $row, array $process = NULL, $value = NULL) {
           $multiple = $multiple || $plugin->multiple();
         }
       }
-      $row->setDestinationProperty($destination, $value);
+      // No plugins means do not set.
+      if ($plugins) {
+        $row->setDestinationProperty($destination, $value);
+      }
       // Reset the value.
       $value = NULL;
     }
   }
 
   /**
-   * Fetch the key array for the current source record.
+   * Fetches the key array for the current source record.
    *
    * @return array
+   *   The current source IDs.
    */
   protected function currentSourceIds() {
     return $this->getSource()->getCurrentIds();
   }
 
   /**
-   * Test whether we've exceeded the designated time limit.
+   * Tests whether we've exceeded the designated time limit.
    *
-   * @return boolean
-   *  TRUE if the threshold is exceeded, FALSE if not.
+   * @return bool
+   *   TRUE if the threshold is exceeded, FALSE if not.
    */
   protected function timeOptionExceeded() {
+    // If there is no time limit, then it is not exceeded.
     if (!$time_limit = $this->getTimeLimit()) {
       return FALSE;
     }
-    $time_elapsed = time() - REQUEST_TIME;
+    // Calculate if the time limit is exceeded.
+    $time_elapsed = $this->getTimeElapsed();
     if ($time_elapsed >= $time_limit) {
       return TRUE;
     }
@@ -387,6 +395,12 @@ protected function timeOptionExceeded() {
     }
   }
 
+  /**
+   * Returns the time limit.
+   *
+   * @return null|int
+   *   The time limit, NULL if no limit or if the units were not in seconds.
+   */
   public function getTimeLimit() {
     $limit = $this->migration->get('limit');
     if (isset($limit['unit']) && isset($limit['value']) && ($limit['unit'] == 'seconds' || $limit['unit'] == 'second')) {
@@ -398,31 +412,31 @@ public function getTimeLimit() {
   }
 
   /**
-   * Pass messages through to the map class.
+   * Passes messages through to the map class.
    *
    * @param string $message
-   *  The message to record.
+   *   The message to record.
    * @param int $level
-   *  Optional message severity (defaults to MESSAGE_ERROR).
+   *   (optional) Message severity (defaults to MESSAGE_ERROR).
    */
   public function saveMessage($message, $level = MigrationInterface::MESSAGE_ERROR) {
     $this->migration->getIdMap()->saveMessage($this->sourceIdValues, $message, $level);
   }
 
   /**
-   * Queue messages to be later saved through the map class.
+   * Queues messages to be later saved through the map class.
    *
    * @param string $message
-   *  The message to record.
+   *   The message to record.
    * @param int $level
-   *  Optional message severity (defaults to MESSAGE_ERROR).
+   *   (optional) Message severity (defaults to MESSAGE_ERROR).
    */
   public function queueMessage($message, $level = MigrationInterface::MESSAGE_ERROR) {
     $this->queuedMessages[] = array('message' => $message, 'level' => $level);
   }
 
   /**
-   * Save any messages we've queued up to the message table.
+   * Saves any messages we've queued up to the message table.
    */
   public function saveQueuedMessages() {
     foreach ($this->queuedMessages as $queued_message) {
@@ -432,14 +446,15 @@ public function saveQueuedMessages() {
   }
 
   /**
-   * Standard top-of-loop stuff, common between rollback and import - check
-   * for exceptional conditions, and display feedback.
+   * Checks for exceptional conditions, and display feedback.
+   *
+   * Standard top-of-loop stuff, common between rollback and import.
    */
   protected function checkStatus() {
     if ($this->memoryExceeded()) {
       return MigrationInterface::RESULT_INCOMPLETE;
     }
-    if ($this->timeExceeded()) {
+    if ($this->maxExecTimeExceeded()) {
       return MigrationInterface::RESULT_INCOMPLETE;
     }
     /*
@@ -463,34 +478,36 @@ protected function checkStatus() {
   }
 
   /**
-   * Test whether we've exceeded the desired memory threshold. If so, output a message.
+   * Tests whether we've exceeded the desired memory threshold.
+   *
+   * If so, output a message.
    *
-   * @return boolean
-   *  TRUE if the threshold is exceeded, FALSE if not.
+   * @return bool
+   *   TRUE if the threshold is exceeded, otherwise FALSE.
    */
   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 > (0.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;
       }
@@ -498,8 +515,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;
       }
@@ -510,36 +527,73 @@ protected function memoryExceeded() {
   }
 
   /**
-   * Test whether we're approaching the PHP time limit.
+   * Returns the memory usage so far.
    *
-   * @return boolean
-   *  TRUE if the threshold is exceeded, FALSE if not.
+   * @return int
+   *   The memory usage.
    */
-  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 getMemoryUsage() {
+    return memory_get_usage();
+  }
+
+  /**
+   * Tries 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 resetting the container.
+    return memory_get_usage();
   }
 
   /**
-   * Takes an Exception object and both saves and displays it, pulling additional
-   * information on the location triggering the exception.
+   * Generates a string representation for the given byte count.
+   *
+   * @param int $size
+   *   A size in bytes.
+   *
+   * @return string
+   *   A translated string representation of the size.
+   */
+  protected function formatSize($size) {
+    return format_size($size);
+  }
+
+  /**
+   * Tests whether we're approaching the PHP maximum execution time limit.
+   *
+   * @return bool
+   *   TRUE if the threshold is exceeded, FALSE if not.
+   */
+  protected function maxExecTimeExceeded() {
+    return $this->maxExecTime && (($this->getTimeElapsed() / $this->maxExecTime) > $this->migration->get('timeThreshold'));
+  }
+
+  /**
+   * Returns the time elapsed.
+   *
+   * This allows a test to set a fake elapsed time.
+   */
+  protected function getTimeElapsed() {
+    return time() - REQUEST_TIME;
+  }
+
+  /**
+   * Takes an Exception object and both saves and displays it.
+   *
+   * Pulls in additional information on the location triggering the exception.
    *
    * @param \Exception $exception
-   *  Object representing the exception.
-   * @param boolean $save
-   *  Whether to save the message in the migration's mapping table. Set to FALSE
-   *  in contexts where this doesn't make sense.
+   *   Object representing the exception.
+   * @param bool $save
+   *   (optional) Whether to save the message in the migration's mapping table.
+   *   Set to FALSE in contexts where this doesn't make sense.
    */
-  public function handleException($exception, $save = TRUE) {
+  public function handleException(\Exception $exception, $save = TRUE) {
     $result = Error::decodeException($exception);
     $message = $result['!message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')';
     if ($save) {
diff --git a/core/modules/migrate/lib/Drupal/migrate/MigrateSkipRowException.php b/core/modules/migrate/lib/Drupal/migrate/MigrateSkipRowException.php
new file mode 100644
index 000000000000..3a019533267b
--- /dev/null
+++ b/core/modules/migrate/lib/Drupal/migrate/MigrateSkipRowException.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\MigrateSkipRowException.
+ */
+
+namespace Drupal\migrate;
+
+/**
+ * This exception is thrown when a row should be skipped.
+ */
+class MigrateSkipRowException extends \Exception {
+
+}
diff --git a/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php
index c1f91c47ff96..4848b9961294 100644
--- a/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php
+++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php
@@ -207,7 +207,7 @@ public function lookupSourceID(array $destination_id_values);
    * @return array
    *   The destination identifier values of the record, or NULL on failure.
    */
-  public function lookupDestinationID(array $source_id_values);
+  public function lookupDestinationId(array $source_id_values);
 
   /**
    * Removes any persistent storage used by this map.
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 44df10cef772..7f9cf0eac83f 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,13 +381,13 @@ public function lookupSourceID(array $destination_id) {
     }
     $result = $query->execute();
     $source_id = $result->fetchAssoc();
-    return array_values($source_id);
+    return array_values($source_id ?: array());
   }
 
   /**
    * {@inheritdoc}
    */
-  public function lookupDestinationID(array $source_id) {
+  public function lookupDestinationId(array $source_id) {
     $query = $this->getDatabase()->select($this->mapTableName, 'map')
               ->fields('map', $this->destinationIdFields);
     foreach ($this->sourceIdFields as $key_name) {
@@ -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,9 +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)) {
-        print($message);
+      // If any key value is not set, we can't save.
+      if (!isset($id_value)) {
         return;
       }
     }
@@ -579,7 +578,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 +694,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 +713,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/FakeConnection.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php
new file mode 100644
index 000000000000..77247148fdde
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\FakeConnection.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Defines a fake connection for use during testing.
+ */
+class FakeConnection extends Connection {
+
+  /**
+   * Table prefix on the database.
+   *
+   * @var string
+   */
+  protected $tablePrefix;
+
+  /**
+   * Connection options for the database.
+   *
+   * @var array
+   */
+  protected $connectionOptions;
+
+  /**
+   * Constructs a FakeConnection.
+   *
+   * @param array $database_contents
+   *   The database contents faked as an array. Each key is a table name, each
+   *   value is a list of table rows.
+   * @param array $connection_options
+   *   (optional) The array of connection options for the database.
+   * @param string $prefix
+   *   (optional) The table prefix on the database.
+   */
+  public function __construct(array $database_contents, $connection_options = array(), $prefix = '') {
+    $this->databaseContents = $database_contents;
+    $this->connectionOptions = $connection_options;
+    $this->tablePrefix = $prefix;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConnectionOptions() {
+    return $this->connectionOptions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tablePrefix($table = 'default') {
+    return $this->tablePrefix;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function select($table, $alias = NULL, array $options = array()) {
+    return new FakeSelect($this->databaseContents, $table, $alias);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function insert($table, array $options = array()) {
+    return new FakeInsert($this->databaseContents, $table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function merge($table, array $options = array()) {
+    return new FakeMerge($this->databaseContents, $table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function update($table, array $options = array()) {
+    return new FakeUpdate($this->databaseContents, $table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function truncate($table, array $options = array()) {
+    return new FakeTruncate($this->databaseContents, $table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function schema() {
+    return new FakeDatabaseSchema($this->databaseContents);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query($query, array $args = array(), $options = array()) {
+    throw new \Exception('Method not supported');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
+    throw new \Exception('Method not supported');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function queryTemporary($query, array $args = array(), array $options = array()) {
+    throw new \Exception('Method not supported');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function driver() {
+    throw new \Exception('Method not supported');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function databaseType() {
+    throw new \Exception('Method not supported');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createDatabase($database) {
+    // There is nothing to do.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mapConditionOperator($operator) {
+    throw new \Exception('Method not supported');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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 bab582fb95af..5c1424dd31e4 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php
@@ -2,10 +2,9 @@
 
 /**
  * @file
- * Contains \Drupal\migrate\Tests\FakeSelect.
+ * Contains \Drupal\migrate\Tests\FakeDatabaseSchema.
  */
 
-
 namespace Drupal\migrate\Tests;
 
 use Drupal\Core\Database\Schema;
@@ -19,11 +18,18 @@ class FakeDatabaseSchema extends Schema {
    */
   public $databaseContents;
 
-  public function __construct($database_contents) {
+  /**
+   * Constructs a fake database schema.
+   *
+   * @param array $database_contents
+   *   The database contents faked as an array. Each key is a table name, each
+   *   value is a list of table rows.
+   */
+  public function __construct(array &$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 000000000000..0074bf3bca30
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\FakeInsert.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\Database\Query\Insert;
+use Drupal\Core\Database\Query\SelectInterface;
+
+/**
+ * Defines FakeInsert for use in database tests.
+ */
+class FakeInsert extends Insert {
+
+  /**
+   * The database contents.
+   *
+   * @var array
+   */
+  protected $databaseContents;
+
+  /**
+   * The database table to insert into.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * Constructs a fake insert object.
+   *
+   * @param array $database_contents
+   *   The database contents faked as an array. Each key is a table name, each
+   *   value is a list of table rows.
+   * @param string $table
+   *   The table to insert into.
+   * @param array $options
+   *   (optional) The database options. Not used.
+   */
+  public function __construct(array &$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 000000000000..09d9e7725d0e
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php
@@ -0,0 +1,61 @@
+<?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;
+
+/**
+ * Defines FakeMerge for use in database tests.
+ */
+class FakeMerge extends Merge {
+
+  /**
+   * Constructs a fake merge object and initializes the conditions.
+   *
+   * @param array $database_contents
+   *   The database contents faked as an array. Each key is a table name, each
+   *   value is a list of table rows.
+   * @param string $table
+   *   The database table to merge into.
+   */
+  function __construct(array &$database_contents, $table) {
+    $this->databaseContents = &$database_contents;
+    $this->table = $table;
+    $this->conditionTable = $table;
+    $this->condition = new Condition('AND');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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 6bf586ed0916..bf49714b130a 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php
@@ -19,7 +19,7 @@ class FakeSelect extends Select {
    * Contents of the pseudo-database.
    *
    * Keys are table names and values are arrays of rows in the table.
-   * Every row there contains all table fields keyed by field name.
+   * Every row there contains all table field values keyed by field name.
    *
    * @code
    * array(
@@ -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 fdcf11310f5a..89954da01056 100644
--- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php
@@ -50,8 +50,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;
   }
 
@@ -90,6 +93,14 @@ public function fetchAllAssoc($key, $fetch = NULL) {
     return $return;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchObject() {
+    $return = $this->fetchAssoc();
+    return $return === FALSE ? FALSE : (object) $return;
+  }
+
   /**
    * {@inheritdoc}
    */
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 000000000000..c1007cd5b644
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\FakeTruncate.
+ */
+
+namespace Drupal\migrate\Tests;
+
+/**
+ * Defines FakeTruncate for use in database tests.
+ */
+class FakeTruncate {
+
+  /**
+   * Constructs a FakeTruncate object.
+   *
+   * @param array $database_contents
+   *   The database contents faked as an array. Each key is a table name, each
+   *   value is a list of table rows.
+   * @param $table
+   *   The table to truncate.
+   */
+  public function __construct(array &$database_contents, $table) {
+    $this->databaseContents = &$database_contents;
+    $this->table = $table;
+  }
+
+  /**
+   * Executes the TRUNCATE query.
+   */
+  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 000000000000..8bf440545cf1
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php
@@ -0,0 +1,88 @@
+<?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;
+
+/**
+ * Defines FakeUpdate for use in database tests.
+ */
+class FakeUpdate extends Update {
+
+  /**
+   * The database table to update.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * The database contents.
+   *
+   * @var array
+   */
+  protected $databaseContents;
+
+  /**
+   * Constructs a FakeUpdate object and initializes the condition.
+   *
+   * @param array $database_contents
+   *   The database contents faked as an array. Each key is a table name, each
+   *   value is a list of table rows.
+   * @param string $table
+   *   The table to update.
+   */
+  public function __construct(array &$database_contents, $table) {
+    $this->databaseContents = &$database_contents;
+    $this->table = $table;
+    $this->condition = new Condition('AND');
+  }
+
+  /**
+   * {@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 80beaacff62f..cd54fd9ae186 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,367 @@ public function testImportWithFailingRewind() {
     $this->assertEquals(MigrationInterface::RESULT_FAILED, $result);
   }
 
-}
+  /**
+   * Tests the import method with a valid row.
+   */
+  public function testImportWithValidRow() {
+    $source = $this->getMockSource();
+
+    $row = $this->getMockBuilder('Drupal\migrate\Row')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $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->executable->setTimeElapsed(1);
+    $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);
+  }
 
-class TestMigrateExecutable extends MigrateExecutable {
+  /**
+   * 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(2);
+    $this->assertTrue($this->executable->maxExecTimeExceeded());
+  }
 
-  public function setTranslationManager(TranslationInterface $translation_manager) {
-    $this->translationManager = $translation_manager;
+  /**
+   * 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));
+  }
+
+  /**
+   * Tests the processRow method with an empty pipeline.
+   */
+  public function testProcessRowEmptyPipeline() {
+    $this->migration->expects($this->once())
+      ->method('getProcessPlugins')
+      ->with(NULL)
+      ->will($this->returnValue(array('test' => array())));
+    $row = new Row(array(), array());
+    $this->executable->processRow($row);
+    $this->assertSame($row->getDestination(), array());
   }
+
+  /**
+   * 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 000000000000..d69873fae0f4
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\MigrateExecuteableMemoryExceeded.
+ */
+
+namespace Drupal\migrate\Tests;
+
+/**
+ * Tests the \Drupal\migrate\MigrateExecutable::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;
+
+  /**
+   * Whether the map is joinable, initialized to FALSE.
+   *
+   * @var bool
+   */
+  protected $mapJoinable = FALSE;
+
+  /**
+   * The migration configuration, initialized to set the ID to test.
+   *
+   * @var array
+   */
+  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 string $message
+   *   The second message to assert.
+   * @param bool $memory_exceeded
+   *   Whether to test the memory exceeded case.
+   * @param int $memory_usage_first
+   *   (optional) The first memory usage value.
+   * @param int $memory_usage_second
+   *   (optional) The fake amount of memory usage reported after memory reclaim.
+   * @param int $memory_limit
+   *   (optional) The memory limit.
+   */
+  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 000000000000..28393febd89a
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php
@@ -0,0 +1,205 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\MigrateSqlIdMapEnsureTablesTest.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+
+/**
+ * Tests the \Drupal\migrate\Plugin\migrate\id_map\Sql::ensureTables() method.
+ *
+ * @group Drupal
+ * @group migrate
+ */
+class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
+
+  /**
+   * Whether the map is joinable, initialized to FALSE.
+   *
+   * @var bool
+   */
+  protected $mapJoinable = FALSE;
+
+  /**
+   * The migration configuration, initialized to set the ID and destination IDs.
+   *
+   * @var array
+   */
+  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',
+    );
+  }
+
+  /**
+   * Tests the ensureTables method when the tables do not exist.
+   */
+  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);
+  }
+
+  /**
+   * Tests the ensureTables method when the tables exist.
+   */
+  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 test.
+   *
+   * @param array $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 000000000000..9bce1722b852
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php
@@ -0,0 +1,741 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\MigrateSqlIdMapTestCase.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\migrate\Entity\MigrationInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Row;
+
+/**
+ * Tests the SQL based ID map implementation.
+ *
+ * @group Drupal
+ * @group migrate
+ */
+class MigrateSqlIdMapTest extends MigrateTestCase {
+
+  /**
+   * Whether the map is joinable, initialized to FALSE.
+   *
+   * @var bool
+   */
+  protected $mapJoinable = FALSE;
+
+  /**
+   * The migration configuration, initialized to set the ID and destination IDs.
+   *
+   * @var array
+   */
+  protected $migrationConfiguration = array(
+    'id' => 'sql_idmap_test',
+    'sourceIds' => array(
+      'source_id_property' => array(),
+    ),
+    'destinationIds' => array(
+      'destination_id_property' => array(),
+    ),
+  );
+
+  /**
+   * The database connection.
+   *
+   * @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
+   *   (optional) 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
+   *   (optional) An array of database connection options.
+   * @param string $prefix
+   *   (optional) The 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);
+  }
+
+  /**
+   * Tests 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) {
+      $id_map->saveMessage(array($key), $message);
+    }
+
+    // Truncate and check that 4 messages were deleted.
+    $this->assertEquals($id_map->messageCount(), 4);
+    $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));
+  }
+
+  /**
+   * Tests 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.");
+    }
+  }
+
+  /**
+   * Tests 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);
+  }
+
+  /**
+   * Tests all the iterator methods in one swing.
+   *
+   * The iterator methods are:
+   * - 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 40d9f639de4e..ef94410ebc46 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 @@ abstract class MigrateTestCase extends UnitTestCase {
    *   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']));
@@ -52,45 +56,21 @@ protected function getMigration() {
   }
 
   /**
-   * @return \Drupal\Core\Database\Connection
+   * Get a fake database connection object for use in tests.
+   *
+   * @param array $database_contents
+   *   The database contents faked as an array. Each key is a table name, each
+   *   value is a list of table rows, an associative array of field => value.
+   * @param array $connection_options
+   *   (optional) The array of connection options for the database.
+   * @param string $prefix
+   *   (optional) The table prefix on the database.
+   *
+   * @return \Drupal\migrate\Tests\FakeConnection
+   *   The 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(array $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 000000000000..2baf52a3af53
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php
@@ -0,0 +1,260 @@
+<?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 {
+
+  /**
+   * The source IDs.
+   *
+   * @var array
+   */
+  protected $testSourceIds = array(
+    'nid' => 'Node ID',
+  );
+
+  /**
+   * The test values.
+   *
+   * @var array
+   */
+  protected $testValues = array(
+    'nid' => 1,
+    'title' => 'node 1',
+  );
+
+  /**
+   * The test hash.
+   *
+   * @var string
+   */
+  protected $testHash = '85795d4cde4a2425868b812cc88052ecd14fc912e7b9b4de45780f66750e8b1e';
+
+  /**
+   * The test hash after changing title value to 'new title'.
+   *
+   * @var string
+   */
+  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 ID Map.
+   *
+   * @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 000000000000..1a222c77df13
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php
@@ -0,0 +1,231 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\TestMigrateExecutable.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\migrate\MigrateExecutable;
+
+/**
+ * Tests MigrateExecutable.
+ */
+class TestMigrateExecutable extends MigrateExecutable {
+
+  /**
+   * The (fake) number of seconds elapsed since the start of the test.
+   *
+   * @var int
+   */
+  protected $timeElapsed;
+
+  /**
+   * The fake memory usage in bytes.
+   *
+   * @var int
+   */
+  protected $memoryUsage;
+
+  /**
+   * The cleared memory usage.
+   *
+   * @var int
+   */
+  protected $clearedMemoryUsage;
+
+  /**
+   * Sets the translation manager.
+   *
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
+   *   The translation manager.
+   */
+  public function setTranslationManager(TranslationInterface $translation_manager) {
+    $this->translationManager = $translation_manager;
+  }
+
+  /**
+   * Allows access to protected timeOptionExceeded method.
+   *
+   * @return bool
+   *   A threshold exceeded value.
+   */
+  public function timeOptionExceeded() {
+    return parent::timeOptionExceeded();
+  }
+
+  /**
+   * Allows access to set protected maxExecTime property.
+   *
+   * @param int $max_exec_time
+   *   The value to set.
+   */
+  public function setMaxExecTime($max_exec_time) {
+    $this->maxExecTime = $max_exec_time;
+  }
+
+  /**
+   * Allows access to protected maxExecTime property.
+   *
+   * @return int
+   *   The value of the protected property.
+   */
+  public function getMaxExecTime() {
+    return $this->maxExecTime;
+  }
+
+  /**
+   * Allows access to protected successesSinceFeedback property.
+   *
+   * @return int
+   *   The value of the protected property.
+   */
+  public function getSuccessesSinceFeedback() {
+    return $this->successesSinceFeedback;
+  }
+
+  /**
+   * Allows access to protected totalSuccesses property.
+   *
+   * @return int
+   *   The value of the protected property.
+   */
+  public function getTotalSuccesses() {
+    return $this->totalSuccesses;
+  }
+
+  /**
+   * Allows access to protected totalProcessed property.
+   *
+   * @return int
+   *   The value of the protected property.
+   */
+  public function getTotalProcessed() {
+    return $this->totalProcessed;
+  }
+
+  /**
+   * Allows access to protected processedSinceFeedback property.
+   *
+   * @var int
+   *   The value of the protected property.
+   */
+  public function getProcessedSinceFeedback() {
+    return $this->processedSinceFeedback;
+  }
+
+  /**
+   * Allows access to protected maxExecTimeExceeded method.
+   *
+   * @return bool
+   *   The threshold exceeded value.
+   */
+  public function maxExecTimeExceeded() {
+    return parent::maxExecTimeExceeded();
+  }
+
+  /**
+   * Allows access to set protected source property.
+   *
+   * @param \Drupal\migrate\Source $source
+   *   The value to set.
+   */
+  public function setSource($source) {
+    $this->source = $source;
+  }
+
+  /**
+   * Allows access to protected sourceIdValues property.
+   *
+   * @param array $source_id_values
+   *   The value to set.
+   */
+  public function setSourceIdValues($source_id_values) {
+    $this->sourceIdValues = $source_id_values;
+  }
+
+  /**
+   * Allows setting a fake elapsed time.
+   *
+   * @param int $time
+   *   The time in seconds.
+   */
+  public function setTimeElapsed($time) {
+    $this->timeElapsed = $time;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTimeElapsed() {
+    return $this->timeElapsed;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handleException(\Exception $exception, $save = TRUE) {
+    $message = $exception->getMessage();
+    if ($save) {
+      $this->saveMessage($message);
+    }
+    $this->message->display($message);
+  }
+
+  /**
+   * Allows access to the protected memoryExceeded method.
+   *
+   * @return bool
+   *   The memoryExceeded value.
+   */
+  public function memoryExceeded() {
+    return parent::memoryExceeded();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function attemptMemoryReclaim() {
+    return $this->clearedMemoryUsage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getMemoryUsage() {
+    return $this->memoryUsage;
+  }
+
+  /**
+   * Sets the fake memory usage.
+   *
+   * @param int $memory_usage
+   *   The fake memory usage value.
+   * @param int $cleared_memory_usage
+   *   (optional) The fake cleared memory value.
+   */
+  public function setMemoryUsage($memory_usage, $cleared_memory_usage = NULL) {
+    $this->memoryUsage = $memory_usage;
+    $this->clearedMemoryUsage = $cleared_memory_usage;
+  }
+
+  /**
+   * Sets the memory limit.
+   *
+   * @param int $memory_limit
+   *   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 000000000000..76f59dbc64f2
--- /dev/null
+++ b/core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdMap.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Tests\TestSqlIdMap.
+ */
+
+namespace Drupal\migrate\Tests;
+
+use Drupal\Core\Database\Connection;
+use Drupal\migrate\Entity\MigrationInterface;
+use Drupal\migrate\Plugin\migrate\id_map\Sql;
+
+/**
+ * Defines a SQL ID map for use in tests.
+ */
+class TestSqlIdMap extends Sql implements \Iterator {
+
+  /**
+   * Constructs a TestSqlIdMap object.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   The database.
+   * @param array $configuration
+   *   The configuration.
+   * @param string $plugin_id
+   *   The plugin ID for the migration process to do.
+   * @param array $plugin_definition
+   *   The configuration for the plugin.
+   * @param \Drupal\migrate\Entity\MigrationInterface $migration
+   *   The migration to do.
+   */
+  function __construct(Connection $database, array $configuration, $plugin_id, array $plugin_definition, MigrationInterface $migration) {
+    $this->database = $database;
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDatabase() {
+    return parent::getDatabase();
+  }
+
+}
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 e49e1f2cc5c4..60e66a77993b 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 39aeb545f9c9..454206fb30a9 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 d99a865b2ab2..923ca97ee5d6 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;
 
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
index 5c6526b02d89..19cabeb2dbf9 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
@@ -38,7 +38,7 @@ function testUserRole() {
     $migrate_test_role_1 = entity_load('user_role', $rid);
     $this->assertEqual($migrate_test_role_1->id(), $rid);
     $this->assertEqual($migrate_test_role_1->getPermissions(), array(0 => 'migrate test role 1 test permission'));
-    $this->assertEqual(array($rid), $migration->getIdMap()->lookupDestinationID(array(3)));
+    $this->assertEqual(array($rid), $migration->getIdMap()->lookupDestinationId(array(3)));
     $rid = 'migrate_test_role_2';
     $migrate_test_role_2 = entity_load('user_role', $rid);
     $this->assertEqual($migrate_test_role_2->getPermissions(), array(
@@ -59,7 +59,7 @@ function testUserRole() {
       'access content overview',
     ));
     $this->assertEqual($migrate_test_role_2->id(), $rid);
-    $this->assertEqual(array($rid), $migration->getIdMap()->lookupDestinationID(array(4)));
+    $this->assertEqual(array($rid), $migration->getIdMap()->lookupDestinationId(array(4)));
   }
 
 }
-- 
GitLab