diff --git a/modules/salesforce_mapping/salesforce_mapping.info b/modules/salesforce_mapping/salesforce_mapping.info
index 1ff9ae6..fb39728 100644
--- a/modules/salesforce_mapping/salesforce_mapping.info
+++ b/modules/salesforce_mapping/salesforce_mapping.info
@@ -6,6 +6,9 @@ core = 7.x
 files[] = includes/salesforce_mapping.entity.inc
 files[] = includes/salesforce_mapping.ui_controller.inc
 files[] = includes/salesforce_mapping_object.entity.inc
+files[] = tests/salesforce_mapping.entities.test
+files[] = tests/salesforce_mapping.map.test
+files[] = tests/salesforce_mapping.test
 
 configure = admin/structure/salesforce/mappings
 
diff --git a/modules/salesforce_mapping/tests/salesforce_mapping.entities.test b/modules/salesforce_mapping/tests/salesforce_mapping.entities.test
new file mode 100644
index 0000000..e772e2f
--- /dev/null
+++ b/modules/salesforce_mapping/tests/salesforce_mapping.entities.test
@@ -0,0 +1,230 @@
+<?php
+
+/**
+ * @file
+ * Simple tests for salesforce_mapping
+ */
+
+module_load_include('test', 'salesforce_mapping', 'tests/salesforce_mapping');
+
+/**
+ * Tests the entities storing the Drupal to Salesforce mapping.
+ */
+class SalesforceMappingEntitiesTestCase extends SalesforceMappingTestCase {
+
+  /**
+   * Implementation of getInfo().
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Mapping Entities',
+      'description' => 'Tests the entities storing the Drupal to Salesforce mapping.',
+      'group' => 'Salesforce',
+    );
+  }
+
+  /**
+   * Implementation of setUp().
+   */
+  public function setUp($modules = array(), $permissions = array()) {
+    parent::setUp($modules, $permissions);
+
+    // Create an example map.
+    $this->example_map = array(
+      'name' => 'foobar',
+      'label' => 'Foo Bar',
+      'type' => 'bazbang',
+      'sync_triggers' => 0x0002,
+      'salesforce_object_type' => 'bar',
+      'salesforce_record_type' => 'baz',
+      'drupal_entity_type' => 'node',
+      'drupal_bundle' => 'story',
+      'field_mappings' => array(
+        'foo' => 'bar',
+        'baz' => array(
+          'bang' => 'boom',
+          'fizz' => 'buzz',
+        ),
+        'hello' => 'world',
+      ),
+      'push_async' => 1,
+      'push_batch' => 1,
+      'weight' => 0,
+      'status' => TRUE,
+    );
+
+    // Create an example record.
+    $this->example_map_object = array(
+      'salesforce_id' => uniqid(),
+      'entity_id' => 3,
+      'entity_type' => 'foobar',
+    );
+  }
+
+  /**
+   * Implementation of tearDown().
+   */
+  public function tearDown() {
+    parent::tearDown();
+  }
+
+  /**
+   * Test the salesforce_mapping entity.
+   */
+  public function testObjectTypeMapEntity() {
+    // Map entity type exists.
+    $entity_info = entity_get_info('salesforce_mapping');
+    $this->assertTrue(isset($entity_info['label']), 'Entity has a label');
+    $this->assertEqual('Salesforce Mapping', $entity_info['label'], 'Entity has expected label.');
+
+    // Create a salesforce_mapping entity object.
+    $test_map = entity_create('salesforce_mapping', $this->example_map);
+    $this->assertEqual('salesforce_mapping', $test_map->entityType(), 'Creating a new entity object works as expected.');
+
+    // Save the entity to the database.
+    $result = entity_save('salesforce_mapping', $test_map);
+    $this->assertEqual(SAVED_NEW, $result, 'Entity saved as new.');
+    $test_map_dbs = entity_load_multiple_by_name('salesforce_mapping', array($this->example_map['name']));
+    $test_map_db = reset($test_map_dbs);
+    $this->assertEqual('bar', $test_map_db->salesforce_object_type, 'Newly created entity has the correct object type.');
+
+    // Save the entity again and see it be updated.
+    $result = entity_save('salesforce_mapping', $test_map);
+    $this->assertEqual(SAVED_UPDATED, $result, 'Re-saved entity saved as updated.');
+
+    // Delete the entity from the database.
+    entity_delete('salesforce_mapping', $this->example_map['name']);
+    $all_entities = entity_load('salesforce_mapping');
+    $this->assertTrue(empty($all_entities));
+  }
+
+  /**
+   * Tests API for interacting with salesforce_mapping.
+   */
+  public function testObjectTypeMapCrud() {
+    // Create two mapping entities to work with.
+    $test_map = entity_create('salesforce_mapping', $this->example_map);
+    entity_save('salesforce_mapping', $test_map);
+    $this->example_map2 = $this->example_map;
+    $this->example_map2['name'] = 'bazbang';
+    $this->example_map2['label'] = 'Baz Bang';
+    $this->example_map2['drupal_bundle'] = 'event';
+    $test_map2 = entity_create('salesforce_mapping', $this->example_map2);
+    entity_save('salesforce_mapping', $test_map2);
+
+    // salesforce_mapping_load() all available.
+    $result = salesforce_mapping_load();
+    $this->assertTrue(is_array($result), 'Loading maps without stating a specific name returned an array.');
+    $this->assertEqual(2, count($result), 'Loading maps without stating a specific name returned 2 maps.');
+
+    // salesforce_mapping_load() specific map.
+    $result = salesforce_mapping_load($test_map->name);
+    $this->assertTrue(is_object($result), 'Loading maps stating a specific name returned an object.');
+    $this->assertEqual($test_map->label, $result->label, 'Loading maps stating a specific name returned the requested map.');
+    $result = salesforce_mapping_load($test_map2->name);
+    $this->assertTrue(is_object($result), 'Loading maps stating a specific name returned an object.');
+    $this->assertEqual($test_map2->label, $result->label, 'Loading maps stating a specific name returned the requested map.');
+
+    // salesforce_mapping_load() retreive nothing.
+    $result = salesforce_mapping_load('nothing');
+    $this->assertFalse(is_object($result), 'Loading maps stating a name that does not exist does not return an object.');
+    $this->assertFalse(is_array($result), 'Loading maps stating a name that does not exist does not return an array.');
+    $this->assertTrue(empty($result), 'Loading maps stating a name that does not exist returns nothing.');
+
+    // salesforce_mapping_load_multiple() all available.
+    $result = salesforce_mapping_load_multiple();
+    $this->assertTrue(is_array($result), 'Loading maps without stating a specific property returned an array.');
+    $this->assertEqual(2, count($result), 'Loading maps without stating a specific property returned 2 maps.');
+
+    // salesforce_mapping_load_multiple() single property.
+    $result = salesforce_mapping_load_multiple(array('drupal_bundle' => 'event'));
+    $this->assertTrue(is_array($result), 'Loading maps stating the drupal_bundle returned an array.');
+    $this->assertEqual(1, count($result), 'Loading maps stating the drupal_bundle property returned 1 map.');
+    $map = reset($result);
+    $this->assertEqual($test_map2->label, $map->label, 'Loading maps stating the drupal_bundle property returned the expected map.');
+    $result = salesforce_mapping_load_multiple(array('drupal_entity_type' => 'node'));
+    $this->assertEqual(2, count($result), 'Loading maps stating the drupal_entity_type property returned 2 maps.');
+
+    // salesforce_mapping_load_multiple() multiple property.
+    $result = salesforce_mapping_load_multiple(array('drupal_entity_type' => 'node', 'drupal_bundle' => 'event'));
+    $this->assertTrue(is_array($result), 'Loading maps stating the drupal_entity_type and drupal_bundle properties returned an array.');
+    $this->assertEqual(1, count($result), 'Loading maps stating the drupal_entity_type and drupal_bundle properties returned 1 map.');
+    $map = reset($result);
+    $this->assertEqual($test_map2->label, $map->label, 'Loading maps stating the drupal_entity_type and drupal_bundle properties returned the expected map.');
+    $result = salesforce_mapping_load_multiple(array('salesforce_object_type' => 'bar', 'salesforce_record_type' => 'baz'));
+    $this->assertEqual(2, count($result), 'Loading maps stating the salesforce_object_type and salesforce_record_type returned 2 maps.');
+
+    // salesforce_mapping_load_multiple() retreive nothing.
+    $result = salesforce_mapping_load_multiple(array('drupal_entity_type' => 'nothing'));
+    $this->assertFalse(is_object($result), 'Loading maps stating a drupal_entity_type that does not exist does not return an object.');
+    $this->assertTrue(is_array($result), 'Loading maps stating a drupal_entity_type that does not exist does returns an array.');
+    $this->assertTrue(empty($result), 'Loading maps stating a drupal_entity_type that does not exist returns an empty array.');
+  }
+
+  /**
+   * Test the salesforce_mapping_object entity.
+   */
+  public function testRecordMapEntity() {
+    // Map entity type exists.
+    $entity_info = entity_get_info('salesforce_mapping_object');
+    $this->assertTrue(isset($entity_info['label']), 'Entity has a label');
+    $this->assertEqual('Salesforce Object Mapping', $entity_info['label'], 'Entity has expected label.');
+
+    // Create a salesforce_mapping_object entity object.
+    $test_map = entity_create('salesforce_mapping_object', $this->example_map_object);
+    $this->assertEqual('salesforce_mapping_object', $test_map->entityType(), 'Creating a new entity object works as expected.');
+
+    // Save the entity to the database.
+    $result = entity_save('salesforce_mapping_object', $test_map);
+    $this->assertEqual(SAVED_NEW, $result, 'Entity saved as new.');
+    $test_map_dbs = entity_load('salesforce_mapping_object', FALSE, array('entity_type' => 'foobar', 'entity_id' => 3));
+    $test_map_db = reset($test_map_dbs);
+    $this->assertEqual($this->example_map_object['salesforce_id'], $test_map_db->salesforce_id, 'Newly created entity has the correct object type.');
+
+    // Save the entity again and see it be updated.
+    $result = entity_save('salesforce_mapping_object', $test_map);
+    $this->assertEqual(SAVED_UPDATED, $result, 'Re-saved entity saved as updated.');
+
+    // Delete the entity from the database.
+    entity_delete('salesforce_mapping_object', $test_map_db->salesforce_mapping_object_id);
+    $all_entities = entity_load('salesforce_mapping_object');
+    $this->assertTrue(empty($all_entities));
+  }
+
+  /**
+   * Tests for salesforce_mapping_crud records.
+   */
+  public function testRecordMapCrud() {
+    // Create two records to work with.
+    $record = array(
+      'salesforce_id' => uniqid(),
+      'entity_id' => 3,
+      'entity_type' => 'foobar',
+    );
+    $record_map = entity_create('salesforce_mapping_object', $record);
+    entity_save('salesforce_mapping_object', $record_map);
+    $record2 = array(
+      'salesforce_id' => uniqid(),
+      'entity_id' => 4,
+      'entity_type' => 'foobar',
+    );
+    $record_map2 = entity_create('salesforce_mapping_object', $record2);
+    entity_save('salesforce_mapping_object', $record_map2);
+
+    // salesforce_mapping_object_load_by_drupal() retreive map.
+    $result = salesforce_mapping_object_load_by_drupal($record_map->entity_type, $record_map->entity_id);
+    $this->assertEqual($record_map->salesforce_id, $result->salesforce_id, 'Loading map by drupal retreived correct map.');
+
+    // salesforce_mapping_object_load_by_drupal() retreive nothing.
+    $result = salesforce_mapping_object_load_by_drupal('nothing', $record_map->entity_id);
+    $this->assertFalse($result, 'Loading map by drupal for something that does not exist returns FALSE.');
+
+    // salesforce_mapping_object_load_by_sfid() retreive map.
+    $result = salesforce_mapping_object_load_by_sfid($record_map->salesforce_id);
+    $this->assertEqual($record_map->entity_id, $result->entity_id, 'Loading map by salesforce_id retreived correct map.');
+
+    // salesforce_mapping_object_load_by_sfid() retreive nothing.
+    $result = salesforce_mapping_object_load_by_sfid('nothing');
+    $this->assertFalse($result, 'Loading map by salesforce_id for something that does not exist returns FALSE.');
+  }
+}
diff --git a/modules/salesforce_mapping/tests/salesforce_mapping.map.test b/modules/salesforce_mapping/tests/salesforce_mapping.map.test
new file mode 100644
index 0000000..55dd90d
--- /dev/null
+++ b/modules/salesforce_mapping/tests/salesforce_mapping.map.test
@@ -0,0 +1,469 @@
+<?php
+
+/**
+ * @file
+ * Tests for mapping salesforce_mapping.
+ */
+
+module_load_include('test', 'salesforce_mapping', 'tests/salesforce_mapping');
+
+/**
+ * Tests the user interface for mapping Drupal entities to Salesforce objects.
+ */
+class SalesforceMappingMapTestCase extends SalesforceMappingTestCase {
+
+  /**
+   * Implementation of getInfo().
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Mapping UI',
+      'description' => 'Tests the user interface for mapping Drupal entities to Salesforce objects.',
+      'group' => 'Salesforce',
+    );
+  }
+
+  /**
+   * Implementation of setUp().
+   */
+  public function setUp($modules = array(), $permissions = array()) {
+    parent::setUp($modules);
+  }
+
+  /**
+   * Implementation of tearDown().
+   */
+  public function tearDown() {
+    parent::tearDown();
+  }
+
+  /**
+   * Tests mapping overview form.
+   */
+  public function testMapOverview() {
+    // Form throws message if it cannot connect to Salesforce.
+    $this->drupalGet($this->adminPath);
+    $this->assertText('You are not authorized to access this page.', 'Message appears when Salesforce is not connected.');
+
+    // Map overview page appears after connecting to Salesforce.
+    $this->salesforceConnect();
+    $this->drupalGet($this->adminPath);
+    $this->assertLink('Add salesforce mapping', 0, 'Add new mapping link appears.');
+    $this->assertLink('Import salesforce mapping', 0, 'Import mapping link appears.');
+    $this->assertRaw('<table id="entity-ui-overview-form"', 'Map listing table appears.');
+    $this->assertRaw('<td colspan="10" class="empty message">None.</td>', 'Map listing table is empty.');
+
+    // Create a map.
+    $this->createSalesforceMapping('foo', 'foobar');
+    $this->drupalGet($this->adminPath);
+    $this->assertRaw('<td>user</td>', 'Drupal entity type shows up in the table.');
+    $this->assertRaw('<td>Contact</td>', 'Salesforce object type shows up in the table.');
+    $this->assertRaw('<span class=\'entity-status-custom\' title=\'A custom configuration by a user.\'>Custom</span>', 'Status declared as custom.');
+    $this->assertRaw('<a href="/admin/structure/salesforce/mappings/manage/foobar">edit</a>', 'Edit link is in table.');
+    $this->assertRaw('<a href="/admin/structure/salesforce/mappings/manage/foobar/clone">clone</a>', 'Clone link is in table.');
+    $this->assertRaw('<a href="/admin/structure/salesforce/mappings/manage/foobar/delete?destination=admin/structure/salesforce/mappings">delete</a>', 'Delete link is in table.');
+    $this->assertRaw('<a href="/admin/structure/salesforce/mappings/manage/foobar/export">export</a>', 'Export link is in table.');
+
+    // Delete map.
+    $this->clickLink('delete');
+    $this->drupalPost(NULL, array(), 'Confirm');
+    $this->assertText('Deleted Salesforce Mapping foo.', 'Delete request posted correctly.');
+    $this->assertRaw('<td colspan="10" class="empty message">None.</td>', 'Map listing table is empty after deleting map.');
+  }
+
+  /**
+   * Tests the AJAX of the mapping form.
+   */
+  public function testMappingAjax() {
+    // Form throws message if it cannot connect to Salesforce.
+    $this->drupalGet($this->addMapPath);
+    $this->assertText('You are not authorized to access this page.', 'Message appears when Salesforce is not connected.');
+
+    // Add map page appears after connecting to Salesforce.
+    $this->salesforceConnect();
+    $this->drupalGet($this->addMapPath);
+    $this->assertFieldById('edit-label', '', 'Label field exists.');
+    $this->assertFieldById('edit-drupal-entity-type', '', 'Drupal entity type field exists.');
+    $this->assertFieldById('edit-salesforce-object-type', '', 'Salesforce object type field exists.');
+    $this->assertFieldById('edit-sync-triggers-1', '', 'Action triggers checkboxes exist.');
+    $this->assertFieldById('edit-push-async', '', 'Push async checkbox exists.');
+    $this->assertFieldById('edit-push-batch', '', 'Push batch checkbox exists.');
+
+    // Verify default values.
+    $this->assertOptionSelected('edit-drupal-entity-type', '', 'Drupal entity type field has correct default value.');
+    $this->assertOptionSelected('edit-salesforce-object-type', '', 'Salesforce object type field has correct default value.');
+    $this->assertNoFieldChecked('edit-sync-triggers-1', 'Trigger on Drupal entity create field has correct default value.');
+    $this->assertNoFieldChecked('edit-sync-triggers-2', 'Trigger on Drupal entity create field has correct default value.');
+    $this->assertNoFieldChecked('edit-sync-triggers-4', 'Trigger on Drupal entity create field has correct default value.');
+    $this->assertNoFieldChecked('edit-sync-triggers-8', 'Trigger on Drupal entity create field has correct default value.');
+    $this->assertNoFieldChecked('edit-sync-triggers-16', 'Trigger on Drupal entity create field has correct default value.');
+    $this->assertNoFieldChecked('edit-sync-triggers-32', 'Trigger on Drupal entity create field has correct default value.');
+    $this->assertNoFieldChecked('edit-push-async', 'Push async field has correct default value.');
+    $this->assertNoFieldChecked('edit-push-batch', 'Push batch field has correct default value.');
+
+    $edit = array();
+
+    // Select a Drupal entity type.
+    $this->assertRaw('<select disabled="disabled" id="edit-drupal-bundle"', 'Drupal bundle field is disabled when Drupal entity type is not selected.');
+    $this->assertText('Select a value for Drupal Entity Type and Drupal Entity Bundle and Salesforce object in order to map fields.', 'Fieldmap give proper initial instructions of what is required to start mapping.');
+    $edit['drupal_entity_type'] = 'user';
+    $this->drupalPostAjax(NULL, $edit, 'drupal_entity_type');
+    $this->assertNoRaw('<select disabled="disabled" id="edit-drupal-bundle"', 'Drupal bundle field is not disabled when Drupal entity type is selected.');
+    $this->assertRaw('<select id="edit-drupal-bundle"', 'Drupal bundle field is not disabled when Drupal entity type is selected.');
+    $this->assertNoText('Select a value for Drupal Entity Type and Drupal Entity Bundle and Salesforce object in order to map fields.', 'Initial fieldmap instructions have been replaced.');
+    $this->assertText('Select a value for Drupal Entity Bundle and Salesforce object in order to map fields.', 'Fieldmap instructions give updated information of what is required to start mapping.');
+
+    // Select a Salesforce object type.
+    $this->assertNoFieldById('edit-salesforce-record-type', '', 'Salesforce record type field does not exist when no object type is selected.');
+    $edit['salesforce_object_type'] = 'Opportunity';
+    $this->drupalPostAjax(NULL, $edit, 'salesforce_object_type');
+    $this->assertFieldById('edit-salesforce-record-type', '', 'Salesforce record type field showed up after object type was selected.');
+    $this->assertNoText('Select a value for Drupal Entity Type and Drupal Entity Bundle and Salesforce object in order to map fields.', 'Initial fieldmap instructions have been replaced.');
+    $this->assertNoText('Select a value for Drupal Entity Bundle and Salesforce object in order to map fields.', 'Updated fieldmap instructions have been replaced again.');
+    $this->assertText('Select a value for Drupal Entity Bundle in order to map fields.', 'Fieldmap instructions give updated information again of what is required to start mapping.');
+
+    // Select a Drupal bundle.
+    $edit['drupal_bundle'] = 'user';
+    $this->assertNoRaw('<table id="edit-salesforce-field-mappings"', 'Field map table does not yet exist.');
+    $this->drupalPostAjax(NULL, $edit, 'drupal_bundle');
+    $this->assertRaw('<table id="edit-salesforce-field-mappings"', 'Field map table has appeared.');
+    $this->assertNoText('Select a value for Drupal Entity Type and Drupal Entity Bundle and Salesforce object in order to map fields.', 'Initial fieldmap instructions have been removed from the page.');
+    $this->assertNoText('Select a value for Drupal Entity Bundle and Salesforce object in order to map fields.', 'Updated fieldmap instructions have been removed from the page.');
+    $this->assertNoText('Select a value for Drupal Entity Bundle in order to map fields.', 'Second updated fieldmap instructions have been removed from the page.');
+    $this->assertFieldById('edit-fieldmap-type-0', '', 'Drupal fieldmap type field has appeared.');
+    $this->assertFieldById('edit-salesforce-field-0', '', 'Salesforce map field has appeared.');
+    $this->assertFieldById('edit-key-0', '', 'Key field has appeared.');
+    $this->assertFieldById('edit-salesforce-field-mappings-0-direction-drupal-sf', '', 'Direction radios have appeared.');
+    $this->assertFieldById('edit-delete-field-mapping-0', '', 'Delete mapping field has appeared.');
+
+    // Unselect the Salesforce object type.
+    $edit['salesforce_object_type'] = '';
+    $this->drupalPostAjax(NULL, $edit, 'salesforce_object_type');
+    $this->assertNoFieldById('edit-salesforce-record-type', '', 'Salesforce record type field disappeared when salesforce object type field was deselected.');
+    $this->assertNoRaw('<table id="edit-salesforce-field-mappings"', 'Field map table disappeared when salesforce object type field was deslected.');
+    $this->assertText('Select a value for Salesforce object in order to map fields.', 'Instructions to select a salesforce object type have appeared.');
+
+    // Reset the Salesforce object type.
+    $edit['salesforce_object_type'] = 'Contact';
+    $this->drupalPostAjax(NULL, $edit, 'salesforce_object_type');
+    $this->assertRaw('<input type="hidden" name="salesforce_record_type" value="default">', 'Salesforce record type his hidden for a salesforce object without records.');
+    $this->assertRaw('<table id="edit-salesforce-field-mappings"', 'Field map table has appeared.');
+
+    // Set the drupal fieldmap type.
+    $edit['salesforce_field_mappings[0][drupal_field][fieldmap_type]'] = 'property';
+    $this->assertNoFieldById('edit-fieldmap-value-0', '', 'Drupal fieldmap value field does not appear when property field is not set.');
+    $this->drupalPostAjax(NULL, $edit, 'salesforce_field_mappings[0][drupal_field][fieldmap_type]');
+    $this->assertFieldById('edit-fieldmap-value-0', '', 'Drupal fieldmap value field has appeared when property field is set.');
+
+    // Add a new row.
+    $this->assertNoFieldById('edit-fieldmap-type-1', '', 'A second row does not exist yet.');
+    $this->drupalPostAjax(NULL, $edit, array('salesforce_add_field' => 'Add another field mapping'));
+    $this->assertFieldById('edit-fieldmap-type-0', '', 'Original row still exists.');
+    $this->assertFieldById('edit-fieldmap-type-1', '', 'Another row appeared after pressing the add field button.');
+    $this->assertOptionSelected('edit-fieldmap-type-0', 'property', 'Original row has retained its previous value.');
+
+    // Add another two rows.
+    $this->assertNoFieldById('edit-fieldmap-type-2', '', 'A third row does not exist yet.');
+    $this->assertNoFieldById('edit-fieldmap-type-3', '', 'A fourth row does not exist yet.');
+    $this->drupalPostAjax(NULL, $edit, array('salesforce_add_field' => 'Add another field mapping'));
+    $this->drupalPostAjax(NULL, $edit, array('salesforce_add_field' => 'Add another field mapping'));
+    $this->assertFieldById('edit-fieldmap-type-0', '', 'Original row still exists.');
+    $this->assertFieldById('edit-fieldmap-type-1', '', 'Second row still exists.');
+    $this->assertFieldById('edit-fieldmap-type-2', '', 'Third row added successfully.');
+    $this->assertFieldById('edit-fieldmap-type-3', '', 'Fourth row added successfully.');
+    $this->assertOptionSelected('edit-fieldmap-type-0', 'property', 'Original row has retained its previous value.');
+    $this->assertOptionSelected('edit-fieldmap-type-1', '', 'Previous new row retained its value.');
+
+    // Delete a row.
+    $edit['delete_field_mapping-1'] = TRUE;
+    $this->drupalPostAjax(NULL, $edit, 'delete_field_mapping-1');
+    unset($edit['delete_field_mapping-1']);
+    $this->assertFieldById('edit-fieldmap-type-0', '', 'First row still exists.');
+    $this->assertNoFieldById('edit-fieldmap-type-1', '', 'Second row successfully deleted.');
+    $this->assertFieldById('edit-fieldmap-type-2', '', 'Third row still exists.');
+    $this->assertFieldById('edit-fieldmap-type-3', '', 'Fourth row still exists.');
+
+    // Add another row.
+    $this->assertNoFieldById('edit-fieldmap-type-1', '', 'The deleted row has not reappeared.');
+    $this->assertNoFieldById('edit-fieldmap-type-4', '', 'A fourth row / 5th key has not appeared.');
+    $this->drupalPostAjax(NULL, $edit, array('salesforce_add_field' => 'Add another field mapping'));
+    $this->assertFieldById('edit-fieldmap-type-0', '', 'Row 0 still exists.');
+    $this->assertNoFieldById('edit-fieldmap-type-1', '', 'Row 1 still deleted.');
+    $this->assertFieldById('edit-fieldmap-type-2', '', 'Row 2 still exists.');
+    $this->assertFieldById('edit-fieldmap-type-3', '', 'Row 3 still exists.');
+    $this->assertFieldById('edit-fieldmap-type-3', '', 'Row 4 has appeared.');
+
+    // Map label and name interaction is done by javascript, not by ajax, and
+    // cannot be tested here.  There is a known interaction failure where if you
+    // show the machine name field and then trigger an ajax event, the machine
+    // name field will not show again, and will not be able to be shown again.
+  }
+
+  /**
+   * Tests the creation of a map.
+   */
+  public function testMappingCreate() {
+    $this->salesforceConnect();
+    $this->createSalesforceMapping('foo', 'foobar');
+
+    // Open the form and verify it reloaded correctly.
+    $this->drupalGet($this->manageMapPrefix . 'foobar');
+    $this->assertFieldById('edit-label', 'foo', 'Label has correct value.');
+    $this->assertFieldById('edit-name', 'foobar', 'Machine name has correct value.');
+    $this->assertOptionSelected('edit-drupal-entity-type', 'user', 'Drupal entity type has correct value.');
+    $this->assertOptionSelected('edit-drupal-bundle', 'user', 'Drupal bundle has correct value.');
+    $this->assertOptionSelected('edit-salesforce-object-type', 'Contact', 'Salesforce object has correct value.');
+    $this->assertFieldByName('salesforce_record_type', 'default', 'Salesforce record type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-type-0', 'property', 'Row 0 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-type-1', 'property', 'Row 1 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-type-2', 'property', 'Row 2 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-0', 'name', 'Row 0 fieldmap value has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-1', 'mail', 'Row 1 fieldmap value has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-2', 'created', 'Row 2 fieldmap value has correct value.');
+    $this->assertOptionSelected('edit-salesforce-field-0', 'Name', 'Row 0 Salesforce field has correct value.');
+    $this->assertOptionSelected('edit-salesforce-field-1', 'Email', 'Row 1 Salesforce field has correct value.');
+    $this->assertOptionSelected('edit-salesforce-field-2', 'CreatedDate', 'Row 2 Salesforce field has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[0][direction]', 'drupal_sf', 'Row 0 direction has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[1][direction]', 'sync', 'Row 1 direction has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[2][direction]', 'sf_drupal', 'Row 2 direction has correct value.');
+    $this->assertRadioOptionSelected('key', 1, 'Key has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-1', 'Trigger on Drupal entity create field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-2', 'Trigger on Drupal entity update field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-4', 'Trigger on Drupal entity delete field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-8', 'Trigger on Salesforce object create field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-16', 'Trigger on Salesforce object update field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-32', 'Trigger on Salesforce object delete field has correct value.');
+    $this->assertFieldChecked('edit-push-async', 'Push async field has correct value.');
+    $this->assertNoFieldChecked('edit-push-batch', 'Push batch field has correct value.');
+
+    // Delete row.
+    $edit = array('delete_field_mapping-2' => TRUE);
+    $this->drupalPostAjax(NULL, $edit, 'delete_field_mapping-2');
+    $this->assertFieldById('edit-fieldmap-type-0', '', 'First row still exists.');
+    $this->assertOptionSelected('edit-fieldmap-value-0', 'name', 'Row 0 fieldmap value still has correct value.');
+    $this->assertFieldById('edit-fieldmap-type-1', '', 'Second row still exists.');
+    $this->assertOptionSelected('edit-fieldmap-value-1', 'mail', 'Row 1 fieldmap value still has correct value.');
+    $this->assertNoFieldById('edit-fieldmap-type-2', '', 'Third row successfully deleted.');
+
+    $edit = array();
+
+    // Add row.Initial_Registration_Date__c
+    $this->drupalPostAjax(NULL, $edit, array('salesforce_add_field' => 'Add another field mapping'));
+    $this->assertFieldById('edit-fieldmap-type-2', '', 'New row now exists.');
+    $edit['salesforce_field_mappings[2][drupal_field][fieldmap_type]'] = 'property';
+    $this->drupalPostAjax(NULL, $edit, 'salesforce_field_mappings[2][drupal_field][fieldmap_type]');
+    $edit['salesforce_field_mappings[2][drupal_field][fieldmap_value]'] = 'created';
+    $edit['salesforce_field_mappings[2][salesforce_field]'] = 'Initial_Registration_Date__c';
+    $edit['salesforce_field_mappings[2][direction]'] = 'drupal_sf';
+
+    // Modify row.
+    $edit['salesforce_field_mappings[0][salesforce_field]'] = 'LastName';
+
+    // Save, verify field alterations remained.
+    $this->drupalPost(NULL, $edit, 'Save mapping');
+    $this->drupalGet($this->manageMapPrefix . 'foobar');
+    $this->assertFieldById('edit-label', 'foo', 'Label has correct value.');
+    $this->assertFieldById('edit-name', 'foobar', 'Machine name has correct value.');
+    $this->assertOptionSelected('edit-drupal-entity-type', 'user', 'Drupal entity type has correct value.');
+    $this->assertOptionSelected('edit-drupal-bundle', 'user', 'Drupal bundle has correct value.');
+    $this->assertOptionSelected('edit-salesforce-object-type', 'Contact', 'Salesforce object has correct value.');
+    $this->assertFieldByName('salesforce_record_type', 'default', 'Salesforce record type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-type-0', 'property', 'Row 0 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-type-1', 'property', 'Row 1 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-type-2', 'property', 'Row 2 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-0', 'name', 'Row 0 fieldmap value has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-1', 'mail', 'Row 1 fieldmap value has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-2', 'created', 'Row 2 fieldmap value has correct value.');
+    $this->assertOptionSelected('edit-salesforce-field-0', 'LastName', 'Row 0 Salesforce field has correct value.');
+    $this->assertOptionSelected('edit-salesforce-field-1', 'Email', 'Row 1 Salesforce field has correct value.');
+    $this->assertOptionSelected('edit-salesforce-field-2', 'Initial_Registration_Date__c', 'Row 2 Salesforce field has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[0][direction]', 'drupal_sf', 'Row 0 direction has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[1][direction]', 'sync', 'Row 1 direction has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[2][direction]', 'drupal_sf', 'Row 2 direction has correct value.');
+    $this->assertRadioOptionSelected('key', 1, 'Key has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-1', 'Trigger on Drupal entity create field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-2', 'Trigger on Drupal entity update field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-4', 'Trigger on Drupal entity delete field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-8', 'Trigger on Salesforce object create field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-16', 'Trigger on Salesforce object update field has correct value.');
+    $this->assertFieldChecked('edit-sync-triggers-32', 'Trigger on Salesforce object delete field has correct value.');
+    $this->assertFieldChecked('edit-push-async', 'Push async field has correct value.');
+    $this->assertNoFieldChecked('edit-push-batch', 'Push batch field has correct value.');
+
+    // Change the Salesforce object type.
+    $edit = array('salesforce_object_type' => 'Account');
+    $this->drupalPostAjax(NULL, $edit, 'salesforce_object_type');
+    $this->assertOptionSelected('edit-fieldmap-type-0', 'property', 'Row 0 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-type-1', 'property', 'Row 1 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-type-2', 'property', 'Row 2 fieldmap type has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-0', 'name', 'Row 0 fieldmap value has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-1', 'mail', 'Row 1 fieldmap value has correct value.');
+    $this->assertOptionSelected('edit-fieldmap-value-2', 'created', 'Row 2 fieldmap value has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[0][direction]', 'drupal_sf', 'Row 0 direction has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[1][direction]', 'sync', 'FieldByName Row 1 direction has correct value.');
+    $this->assertRadioOptionSelected('salesforce_field_mappings[2][direction]', 'drupal_sf', 'Row 2 direction has correct value.');
+    $this->assertRadioOptionSelected('key', 1, 'Key has correct value.');
+
+    // Change the Drupal entity type.
+    $edit['drupal_entity_type'] = 'node';
+    $this->drupalPostAjax(NULL, $edit, 'drupal_entity_type');
+    $this->assertText('Select a value for Drupal Entity Bundle in order to map fields.', 'Fieldmap instructions give updated information again of what is required to start mapping.');
+  }
+
+  /**
+   * Test validation of the mapping form.
+   */
+  public function testMappingValidation() {
+    $this->salesforceConnect();
+    $this->createSalesforceMapping('foo', 'foobar');
+
+    // Cannot map the same entity, bundle and salesforce object combo.
+    $edit = array();
+    $this->drupalGet($this->addMapPath);
+    $edit['drupal_entity_type'] = 'user';
+    $this->drupalPostAjax(NULL, $edit, 'drupal_entity_type');
+    $edit['drupal_bundle'] = 'user';
+    $this->drupalPostAjax(NULL, $edit, 'drupal_bundle');
+    $edit['salesforce_object_type'] = 'Contact';
+    $this->drupalPostAjax(NULL, $edit, 'salesforce_object_type');
+    $this->drupalPost(NULL, $edit, 'Save mapping');
+    $this->assertText('This Drupal bundle has already been mapped to a Salesforce object', 'Validation failed when a new map was created to map the same entity, bundle an salesforce object that matched a previous mapping.');
+
+    // Label cannot exceed SALESFORCE_MAPPING_NAME_LENGTH.
+    $this->drupalGet($this->manageMapPrefix . 'foobar');
+    $length = SALESFORCE_MAPPING_NAME_LENGTH + 1;
+    $edit = array('label' => $this->randomName($length));
+    $this->drupalPost(NULL, $edit, 'Save mapping');
+    $this->assertText('Label cannot be longer than 128 characters but is currently 129 characters long.', 'Validation rejected label length that was too long.');
+
+    // When sync trigger Salesforce Create selected, at least one row must have
+    // direction 'sync' or 'sf_drupal'.
+    $edit = array(
+      'salesforce_field_mappings[0][direction]' => 'drupal_sf',
+      'salesforce_field_mappings[1][direction]' => 'drupal_sf',
+      'salesforce_field_mappings[2][direction]' => 'drupal_sf',
+      'sync_triggers[8]' => TRUE,
+    );
+    $this->drupalPost(NULL, $edit, 'Save mapping');
+    $this->assertText('One mapping must be set to "Sync" or "SF to Drupal" when "Salesforce object create" is selected', 'Validation rejected when salesforce create sync trigger is selected and no row had either "sync" or "sf to drupal" direction selected.');
+
+    // Key must be set on a field that is configured to be externalId.
+    $edit = array('key' => '0');
+    $this->drupalPost(NULL, $edit, 'Save mapping');
+    $this->assertText('is not configured as an external id', 'Validation rejected when row selected as "key" was not a salesforce field with a valid external id field.');
+    $edit = array('key' => '2');
+    $this->drupalPost(NULL, $edit, 'Save mapping');
+    $this->assertText('s not configured as an external id', 'Validation rejected when row selected as "key" was not a salesforce field with a valid external id field.');
+
+    // Verify salesforce_mapping_property_validation().
+    $property_tests = array(
+      'sync' => array(
+        'datetime' => array(
+          'sf_value' => 'CreatedDate',
+          'drupal_value' => array(
+            'integer' => 'uid',
+            'text' => 'name',
+            'uri' => 'url',
+          ),
+        ),
+        'email' => array(
+          'sf_value' => 'Email',
+          'drupal_value' => array(
+            'date' => 'created',
+            'integer' => 'uid',
+            'uri' => 'url',
+          ),
+        ),
+        'id' => array(
+          'sf_value' => 'Id',
+          'drupal_value' => array(
+            'date' => 'created',
+            'uri' => 'url',
+          ),
+        ),
+        'string' => array(
+          'sf_value' => 'LastName',
+          'drupal_value' => array(
+            'date' => 'created',
+            'integer' => 'uid',
+          ),
+        ),
+      ),
+      'drupal_sf' => array(
+        'datetime' => array(
+          'sf_value' => 'CreatedDate',
+          'drupal_value' => array(
+            'integer' => 'uid',
+            'text' => 'name',
+            'uri' => 'url',
+          ),
+        ),
+        'email' => array(
+          'sf_value' => 'Email',
+          'drupal_value' => array(
+            'date' => 'created',
+            'integer' => 'uid',
+            'uri' => 'url',
+          ),
+        ),
+        'id' => array(
+          'sf_value' => 'Id',
+          'drupal_value' => array(
+            'date' => 'created',
+            'uri' => 'url',
+          ),
+        ),
+        'string' => array(
+          'sf_value' => 'LastName',
+          'drupal_value' => array(),
+        ),
+      ),
+      'sf_drupal' => array(
+        'datetime' => array(
+          'sf_value' => 'CreatedDate',
+          'drupal_value' => array(
+            'integer' => 'uid',
+            'text' => 'name',
+            'uri' => 'url',
+          ),
+        ),
+        'email' => array(
+          'sf_value' => 'Email',
+          'drupal_value' => array(
+            'date' => 'created',
+            'integer' => 'uid',
+            'uri' => 'url',
+          ),
+        ),
+        'id' => array(
+          'sf_value' => 'Id',
+          'drupal_value' => array(
+            'date' => 'created',
+            'uri' => 'url',
+          ),
+        ),
+        'string' => array(
+          'sf_value' => 'LastName',
+          'drupal_value' => array(
+            'date' => 'created',
+            'uri' => 'url',
+          ),
+        ),
+      ),
+    );
+    foreach ($property_tests as $direction => $sf_field_types) {
+      foreach ($sf_field_types as $sf_field_type => $data) {
+        $sf_field = $data['sf_value'];
+        foreach ($data['drupal_value'] as $drupal_field_type => $drupal_field) {
+          $edit = array(
+            'salesforce_field_mappings[0][direction]' => $direction,
+            'salesforce_field_mappings[0][salesforce_field]' => $sf_field,
+            'salesforce_field_mappings[0][drupal_field][fieldmap_value]' => $drupal_field,
+          );
+          $this->drupalPost(NULL, $edit, 'Save mapping');
+          $this->assertText('and cannot be mapped in the ' . $direction . ' direction', 'Validation failed when direction is "' . $direction . '", salesforce field type is "' . $sf_field_type . '" and drupal field type is "' . $drupal_field_type . '".');
+        }
+      }
+    }
+  }
+}
diff --git a/modules/salesforce_mapping/tests/salesforce_mapping.test b/modules/salesforce_mapping/tests/salesforce_mapping.test
new file mode 100644
index 0000000..fc73e00
--- /dev/null
+++ b/modules/salesforce_mapping/tests/salesforce_mapping.test
@@ -0,0 +1,411 @@
+<?php
+
+/**
+ * @file
+ * Simple tests for salesforce_mapping
+ */
+
+module_load_include('test', 'salesforce', 'tests/salesforce');
+
+/**
+ * Sets up basic tools for the testing of mapping Drupal to Salesforce.
+ */
+class SalesforceMappingTestCase extends SalesforceTestCase {
+
+  protected $adminPath = 'admin/structure/salesforce/mappings';
+  protected $addMapPath = 'admin/structure/salesforce/mappings/add';
+  protected $manageMapPrefix = 'admin/structure/salesforce/mappings/manage/';
+
+  /**
+   * Implementation of getInfo().
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Salesforce Mapping',
+      'description' => 'Sets up basic tools for the testing of mapping Drupal to Salesforce.',
+      'group' => 'Salesforce',
+    );
+  }
+
+  /**
+   * Implementation of setUp().
+   */
+  public function setUp($modules = array(), $permissions = array()) {
+    $modules = array_merge($modules, array(
+      'entity',
+      'salesforce_mapping',
+    ));
+    $permissions = array_merge($permissions, array(
+      'administer salesforce mapping',
+      'view salesforce mapping',
+    ));
+    parent::setUp($modules, $permissions);
+  }
+
+  /**
+   * Implementation of tearDown().
+   */
+  public function tearDown() {
+    parent::tearDown();
+  }
+
+  /**
+   * Submits a salesforce mapping form of your configuration or a default one.
+   *
+   * @param string $label
+   *   Desired lable for the mapping.
+   * @param string $machine_name
+   *   Desired machine name of the mapping.  If none is provided, one will be
+   *   automatically generated from the label.
+   * @param array $config
+   *   Desired mapping configuration.  If none is provided, a default mapping
+   *   configuration will be generated.
+   */
+  protected function createSalesforceMapping($label, $machine_name = NULL, $config = array()) {
+    // Give a default configuration if one is not provided for us.
+    if (empty($config)) {
+      $config = array(
+        'drupal_entity_type' => 'user',
+        'drupal_bundle' => 'user',
+        'salesforce_object_type' => 'Contact',
+        'key' => 1,
+        'mapping' => array(
+          array(
+            'fieldmap_type' => 'property',
+            'fieldmap_value' => 'name',
+            'salesforce_field' => 'Name',
+            'direction' => 'drupal_sf',
+          ),
+          array(
+            'fieldmap_type' => 'property',
+            'fieldmap_value' => 'mail',
+            'salesforce_field' => 'Email',
+            'direction' => 'sync',
+          ),
+          array(
+            'fieldmap_type' => 'property',
+            'fieldmap_value' => 'name',
+            'salesforce_field' => 'LastName',
+            'direction' => 'sf_drupal',
+          ),
+        ),
+        'sync_triggers' => array(
+          '1' => TRUE,
+          '2' => TRUE,
+          '4' => TRUE,
+          '8' => TRUE,
+          '16' => TRUE,
+          '32' => TRUE,
+        ),
+        'push_async' => TRUE,
+      );
+    }
+
+    $edit = array();
+    $machine_name = is_null($machine_name) ? str_replace(' ', '_', strtolower($label)) : $machine_name;
+    $this->drupalGet($this->addMapPath);
+    $this->assertNoText('You are not authorized to access this page.', 'Able to access the create map page.');
+
+    // Get all of the AJAX behaviors out of the way.
+    $edit['drupal_entity_type'] = $config['drupal_entity_type'];
+    $this->drupalPostAjax(NULL, $edit, 'drupal_entity_type');
+    unset($config['drupal_entity_type']);
+    $edit['drupal_bundle'] = $config['drupal_bundle'];
+    $this->drupalPostAjax(NULL, $edit, 'drupal_bundle');
+    unset($config['drupal_bundle']);
+    $edit['salesforce_object_type'] = $config['salesforce_object_type'];
+    $this->drupalPostAjax(NULL, $edit, 'salesforce_object_type');
+    unset($config['salesforce_object_type']);
+    foreach ($config['mapping'] as $delta => $map) {
+      $edit['salesforce_field_mappings[' . $delta . '][drupal_field][fieldmap_type]'] = $map['fieldmap_type'];
+      $this->drupalPostAjax(NULL, $edit, 'salesforce_field_mappings[' . $delta . '][drupal_field][fieldmap_type]');
+      $this->drupalPostAjax(NULL, $edit, array('salesforce_add_field' => 'Add another field mapping'));
+      unset($config['mapping'][$delta]['fieldmap_type']);
+    }
+
+    // Fill out the rest of the form.
+    $edit['label'] = $label;
+    $edit['name'] = $machine_name;
+    foreach ($config as $key => $data) {
+      switch ($key) {
+        case 'mapping':
+          foreach ($data as $delta => $fields) {
+            foreach ($fields as $field => $value) {
+              if ($field == 'fieldmap_value') {
+                $edit['salesforce_field_mappings[' . $delta . '][drupal_field][fieldmap_value]'] = $value;
+              }
+              else {
+                $edit['salesforce_field_mappings[' . $delta . '][' . $field . ']'] = $value;
+              }
+            }
+          }
+          break;
+
+        case 'sync_triggers':
+          foreach ($data as $value => $flag) {
+            $edit['sync_triggers[' . $value . ']'] = $flag;
+          }
+          break;
+
+        default:
+          $edit[$key] = $data;
+      }
+    }
+
+    // Submit form.
+    $this->drupalPost(NULL, $edit, 'Save mapping');
+    $this->assertText('Salesforce field mapping saved.', 'Form posted as expected.');
+    $this->assertRaw('id="salesforce-mapping-overview-form"', 'Redirected to the mappings overview table.');
+    $this->assertRaw('(Machine name: ' . $machine_name . ')', 'New map successfully appears on overview page.');
+    $this->assertLink($label, 0, 'Link to edit new map appears.');
+  }
+
+  /**
+   * Execute an Ajax submission.
+   *
+   * This is unfortunately a modification of the original drupalPostAjax because
+   * it did not have support for cases where ajax commands were set manually.  I
+   * have added support for when a wrapper is declared as a single id.
+   *
+   * @param string $path
+   *   Location of the form containing the Ajax enabled element to test. Can be
+   *   either a Drupal path or an absolute path or NULL to use the current page.
+   * @param array $edit
+   *   Field data in an associative array. Changes the current input fields
+   *   (where possible) to the values indicated.
+   * @param string $triggering_element
+   *   The name of the form element that is responsible for triggering the Ajax
+   *   functionality to test. May be a string or, if the triggering element is
+   *   a button, an associative array where the key is the name of the button
+   *   and the value is the button label. i.e.) array('op' => t('Refresh')).
+   * @param string $ajax_path
+   *   (optional) Override the path set by the Ajax settings of the triggering
+   *   element. In the absence of both the triggering element's Ajax path and
+   *   $ajax_path 'system/ajax' will be used.
+   * @param array $options
+   *   (optional) Options to be forwarded to url().
+   * @param array $headers
+   *   (optional) An array containing additional HTTP request headers, each
+   *   formatted as "name: value". Forwarded to drupalPost().
+   * @param string $form_html_id
+   *   (optional) HTML ID of the form to be submitted, use when there is more
+   *   than one identical form on the same page and the value of the triggering
+   *   element is not enough to identify the form. Note this is not the Drupal
+   *   ID of the form but rather the HTML ID of the form.
+   * @param array $ajax_settings
+   *   (optional) An array of Ajax settings which if specified will be used in
+   *   place of the Ajax settings of the triggering element.
+   *
+   * @return array
+   *   An array of Ajax commands.
+   */
+  protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) {
+    // Get the content of the initial page prior to calling drupalPost(), since
+    // drupalPost() replaces $this->content.
+    if (isset($path)) {
+      $this->drupalGet($path, $options);
+    }
+    $content = $this->content;
+    $drupal_settings = $this->drupalSettings;
+
+    // Get the Ajax settings bound to the triggering element.
+    if (!isset($ajax_settings)) {
+      if (is_array($triggering_element)) {
+        $xpath = '//*[@name="' . key($triggering_element) . '" and @value="' . current($triggering_element) . '"]';
+      }
+      else {
+        $xpath = '//*[@name="' . $triggering_element . '"]';
+      }
+      if (isset($form_html_id)) {
+        $xpath = '//form[@id="' . $form_html_id . '"]' . $xpath;
+      }
+      $element = $this->xpath($xpath);
+      $element_id = (string) $element[0]['id'];
+      $ajax_settings = $drupal_settings['ajax'][$element_id];
+    }
+
+    // Add extra information to the POST data as ajax.js does.
+    $extra_post = '';
+    if (isset($ajax_settings['submit'])) {
+      foreach ($ajax_settings['submit'] as $key => $value) {
+        $extra_post .= '&' . urlencode($key) . '=' . urlencode($value);
+      }
+    }
+    foreach ($this->xpath('//*[@id]') as $element) {
+      $id = (string) $element['id'];
+      $extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id);
+    }
+    if (isset($drupal_settings['ajaxPageState'])) {
+      $extra_post .= '&' . urlencode('ajax_page_state[theme]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme']);
+      $extra_post .= '&' . urlencode('ajax_page_state[theme_token]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme_token']);
+      foreach ($drupal_settings['ajaxPageState']['css'] as $key => $value) {
+        $extra_post .= '&' . urlencode("ajax_page_state[css][$key]") . '=1';
+      }
+      foreach ($drupal_settings['ajaxPageState']['js'] as $key => $value) {
+        $extra_post .= '&' . urlencode("ajax_page_state[js][$key]") . '=1';
+      }
+    }
+
+    // Unless a particular path is specified, use the one specified by the
+    // Ajax settings, or else 'system/ajax'.
+    if (!isset($ajax_path)) {
+      $ajax_path = isset($ajax_settings['url']) ? $ajax_settings['url'] : 'system/ajax';
+    }
+
+    // Submit the POST request.
+    $return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post));
+
+    // Change the page content by applying the returned commands.
+    if (!empty($ajax_settings) && !empty($return)) {
+      // ajax.js applies some defaults to the settings object, so do the same
+      // for what's used by this function.
+      $ajax_settings += array(
+        'method' => 'replaceWith',
+      );
+      // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
+      // them.
+      $dom = new DOMDocument();
+      @$dom->loadHTML($content);
+      // XPath allows for finding wrapper nodes better than DOM does.
+      $xpath = new DOMXPath($dom);
+      foreach ($return as $command) {
+        switch ($command['command']) {
+          case 'settings':
+            $drupal_settings = drupal_array_merge_deep($drupal_settings, $command['settings']);
+            break;
+
+          case 'insert':
+            $wrapper_node = NULL;
+            // When a command doesn't specify a selector, use the
+            // #ajax['wrapper'] which is always an HTML ID.
+            if (!isset($command['selector'])) {
+              $wrapper_node = $xpath->query('//*[@id="' . $ajax_settings['wrapper'] . '"]')->item(0);
+            }
+            // @todo Ajax commands can target any jQuery selector, but these are
+            //   hard to fully emulate with XPath. For now, just handle 'head'
+            //   and 'body', since these are used by ajax_render().
+            elseif (in_array($command['selector'], array('head', 'body'))) {
+              $wrapper_node = $xpath->query('//' . $command['selector'])->item(0);
+            }
+            // Begin modification.  When a custom command declares a single id
+            // as its selector.
+            elseif (substr($command['selector'], 0, 1) == '#') {
+              $wrapper_node = $xpath->query('//*[@id="' . substr($command['selector'], 1) . '"]')->item(0);
+            }
+            // End modification.
+            if ($wrapper_node) {
+              // ajax.js adds an enclosing DIV to work around a Safari bug.
+              $new_dom = new DOMDocument();
+              $new_dom->loadHTML('<div>' . $command['data'] . '</div>');
+              $new_node = $dom->importNode($new_dom->documentElement->firstChild->firstChild, TRUE);
+              $method = isset($command['method']) ? $command['method'] : $ajax_settings['method'];
+              // The "method" is a jQuery DOM manipulation function. Emulate
+              // each one using PHP's DOMNode API.
+              switch ($method) {
+                case 'replaceWith':
+                  $wrapper_node->parentNode->replaceChild($new_node, $wrapper_node);
+                  break;
+
+                case 'append':
+                  $wrapper_node->appendChild($new_node);
+                  break;
+
+                case 'prepend':
+                  // If no firstChild, insertBefore() falls back to
+                  // appendChild().
+                  $wrapper_node->insertBefore($new_node, $wrapper_node->firstChild);
+                  break;
+
+                case 'before':
+                  $wrapper_node->parentNode->insertBefore($new_node, $wrapper_node);
+                  break;
+
+                case 'after':
+                  // If no nextSibling, insertBefore() falls back to
+                  // appendChild().
+                  $wrapper_node->parentNode->insertBefore($new_node, $wrapper_node->nextSibling);
+                  break;
+
+                case 'html':
+                  foreach ($wrapper_node->childNodes as $child_node) {
+                    $wrapper_node->removeChild($child_node);
+                  }
+                  $wrapper_node->appendChild($new_node);
+                  break;
+              }
+            }
+            break;
+        }
+      }
+      $content = $dom->saveHTML();
+    }
+    $this->drupalSetContent($content);
+    $this->drupalSetSettings($drupal_settings);
+    return $return;
+  }
+
+  /**
+   * Assert if the given radio field has the given value selected.
+   *
+   * I could not find a reliable way of ensuring that a radio option was
+   * actually checked.  I added these helper asserts to streamline the testing
+   * of the mapping forms.
+   *
+   * @param string $name
+   *   Name of field to assert.
+   * @param string $value
+   *   (optional) Value of the field to assert.
+   * @param string $message
+   *   (optional) Message to display.
+   * @param string $group
+   *   (optional) The group this message belongs to.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertRadioOptionSelected($name, $value, $message = '', $group = 'Other') {
+    $matches = $this->checkRadioOptionSelected($name, $value);
+    return $this->assertTrue($matches, $message, $group);
+  }
+
+  /**
+   * Assert if the given radio field does not have the given value selected.
+   *
+   * I could not find a reliable way of ensuring that a radio option was
+   * actually checked.  I added these helper asserts to streamline the testing
+   * of the mapping forms.
+   *
+   * @param string $name
+   *   Name of field to assert.
+   * @param string $value
+   *   (optional) Value of the field to assert.
+   * @param string $message
+   *   (optional) Message to display.
+   * @param string $group
+   *   (optional) The group this message belongs to.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   */
+  protected function assertNoRadioOptionSelected($name, $value, $message = '', $group = 'Other') {
+    $matches = $this->checkRadioOptionSelected($name, $value);
+    return $this->assertFalse($matches, $message, $group);
+  }
+
+  /**
+   * Helper to see if the given value is selected for the given radio field.
+   */
+  protected function checkRadioOptionSelected($name, $value) {
+    $fields = $this->xpath($this->constructFieldXpath('name', $name));
+    if (is_array($fields)) {
+      foreach ($fields as $field) {
+        if (isset($field['checked']) && $field['checked'] == 'checked') {
+          if ($field['value'] == $value) {
+            return TRUE;
+          }
+        }
+      }
+    }
+    return FALSE;
+  }
+}
diff --git a/salesforce.info b/salesforce.info
index c748d9c..baeabb3 100644
--- a/salesforce.info
+++ b/salesforce.info
@@ -7,5 +7,6 @@ configure = admin/config/salesforce
 
 files[] = includes/salesforce.inc
 files[] = includes/salesforce.select_query.inc
+files[] = tests/salesforce.test
 
 php = 5.3
diff --git a/tests/salesforce.test b/tests/salesforce.test
new file mode 100644
index 0000000..8306379
--- /dev/null
+++ b/tests/salesforce.test
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * @file
+ * Simple tests for salesforce
+ */
+
+/**
+ * Sets up basic tools for testing Salesforce integration.
+ */
+class SalesforceTestCase extends DrupalWebTestCase {
+
+  /**
+   * Implementation of getInfo().
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Salesforce API',
+      'description' => 'Sets up basic tools for testing Salesforce integration',
+      'group' => 'Salesforce',
+    );
+  }
+
+  /**
+   * Implementation of setUp().
+   */
+  public function setUp($modules = array(), $permissions = array()) {
+    $modules = array_merge($modules, array(
+      'salesforce',
+    ));
+    parent::setUp($modules);
+
+    $permissions = array_merge($permissions, array(
+      'administer salesforce',
+    ));
+    $this->admin_user = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->admin_user);
+  }
+
+  /**
+   * Implementation of tearDown().
+   */
+  public function tearDown() {
+    user_cancel(array(), $this->admin_user->uid, 'user_cancel_delete');
+    parent::tearDown();
+  }
+
+  /**
+   * Grabs Salesforce connection information from the live db and authenticates.
+   *
+   * @return object
+   *   Salesforce Object
+   */
+  protected function salesforceConnect() {
+    // Steal the Salesforce configuration from the live database.
+    global $db_prefix;
+    $table = empty($db_prefix) ? 'variable' : $db_prefix . '_variable';
+    $salesforce_vars = array(
+      'salesforce_consumer_key',
+      'salesforce_consumer_secret',
+      'salesforce_instance_url',
+      'salesforce_refresh_token',
+      'salesforce_identity',
+    );
+    $vars = "'" . implode("', '", $salesforce_vars) . "'";
+    $sql = "SELECT v.name, v.value FROM $table v WHERE v.name IN ($vars)";
+    $result = db_query($sql);
+    foreach ($result as $record) {
+      variable_set($record->name, unserialize($record->value));
+    }
+
+    // Test the connection.
+    $sfapi = salesforce_get_api();
+    if ($sfapi->isAuthorized()) {
+      $this->Pass('Connected to Salesforce');
+    }
+    else {
+      $this->Fail('Could not connect to Salesforce.  Ensure the primary site these tests are running against has an authorized connection to Salesforce in order to proceed.');
+    }
+
+    // Make a call to Salesforce that will do nothing just so we can get an
+    // access token into our session.  This is dumb, but I can't think of any
+    // other way to get an access token because refreshToken() is protected.
+    $sfapi->apiCall('');
+
+    return $sfapi;
+  }
+}
