diff --git a/project_src_github.info b/project_src_github.info
index 82deef7..f42b73a 100644
--- a/project_src_github.info
+++ b/project_src_github.info
@@ -5,3 +5,5 @@ package = Development
 configure = admin/config/development/github-src
 dependencies[] = project_src
 dependencies[] = github_api
+
+files[] = project_src_github.test
diff --git a/project_src_github.test b/project_src_github.test
new file mode 100644
index 0000000..ee70464
--- /dev/null
+++ b/project_src_github.test
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * @file
+ * SimpleTest tests for the Project Source: GitHub module.
+ */
+
+
+/**
+ * Helper class with some added functions and properties for testing.
+ */
+class ProjectSrcGithubBaseCase extends DrupalWebTestCase {
+
+  /**
+   * The GitHub organization used for testing.
+   */
+  protected $org = 'drewpaul_inc';
+
+  /**
+   * @var GithubApiMock
+   */
+  protected $github;
+
+  function setUp(array $modules = array()) {
+    $modules[] = 'project_src';
+    $modules[] = 'project_src_github';
+    parent::setUp($modules);
+
+    // Order matters!
+    $fake_api = 'project_src_github_test_github_api';
+    $this->fakeModuleUninstall('composer_manager');
+    $this->fakeModuleUninstall('github_api');
+    drupal_flush_all_caches();
+    $this->fakeModuleInstall($fake_api);
+
+    // Set the organization variable.
+    variable_set('project_src_github_org', $this->org);
+
+    // Keep track of our fake GitHub API object for test comparisons.
+    module_load_include('module', $fake_api, $fake_api);
+    $this->github = new GithubApiMock();
+  }
+
+  /**
+   * Compares the provided project XML values with those provided by our mock
+   * GitHub API object.
+   *
+   * @param SimpleXMLElement|array $xml
+   *   The SimpleXMLElement as returned by the xpath query (or an array that
+   *   contains at a SimpleXMLElement as its first item).
+   */
+  protected function validateProjectXml($xml) {
+    $repos = $this->github->repositories($this->org);
+    $repo = $repos[$this->org . '_module'];
+
+    // We may be handed an array.
+    if (is_array($xml)) {
+      $xml = $xml[0];
+    }
+
+    $this->assertEqual($repo['name'], $xml->title, 'Returned the correct project title.');
+    $this->assertEqual($repo['name'], $xml->short_name, 'Returned the correct project short name.');
+    $this->assertEqual($repo['owner']['login'], $xml->creator, 'Returned the correct project creator');
+    // @todo Api version?
+    // @todo Recommended/supported/default major?
+    // @todo Status?
+    // @todo Link?
+  }
+
+  /**
+   * Compares the provided release XML values with those provided by our mock
+   * GitHub API object.
+   *
+   * @param SimpleXMLElement|array $xml
+   *   The SimpleXMLElement as returned by the xpath query (or an array that
+   *   contains at a SimpleXMLElement as its first item).
+   */
+  protected function validateReleaseXml($xml) {
+    $repos = $this->github->repositories($this->org);
+    $repo = $repos[$this->org . '_module'];
+    $branches = $this->github->branches($this->org, $this->org . '_module');
+    $commit = $this->github->show($this->org, $this->org . '_module', '');
+
+    // Tests for if we're dealing with a dev release.
+    if (substr($xml->version, -4) == '-dev') {
+      $name = $this->org . '_module ' . $xml->version;
+      $real_tag = substr($xml->version, 0, -4);
+      $major_version = substr($real_tag, 4, 1);
+      $file_name = 'project-src-github/' . $repo['full_name'] . '/' . $real_tag . '/' . 'download';
+      $this->assertTrue(isset($branches[$real_tag]), 'Release matches a valid branch.');
+      $this->assertTrue($name == $xml->name, 'Returned the correct release name.');
+      $this->assertTrue($xml->version == $xml->tag, 'Returned the correct release tag.');
+      $this->assertTrue($xml->version_major == $major_version, 'Returned the correct major version.');
+      $this->assertTrue($xml->version_extra == 'dev', 'Returned correct extra version metadata.');
+      $this->assertTrue(strpos($xml->download_link, $file_name) !== FALSE, 'Returned valid release download link.');
+      $this->assertTrue($xml->date == strtotime($commit['commit']['author']['date']), 'Returned valid release date.');
+      // @todo Release link?
+      // @todo Status?
+    }
+  }
+
+  /**
+   * Performs an unofficial module install (in that no hooks are run and no
+   * dependencies are loaded or installed).
+   *
+   * @param string $module
+   *   The module to "install."
+   */
+  protected function fakeModuleInstall($module) {
+    db_update('system')
+      ->fields(array('status' => 1))
+      ->condition('type', 'module')
+      ->condition('name', $module)
+      ->execute();
+
+    system_list_reset();
+    module_list(TRUE);
+    module_implements('', FALSE, TRUE);
+    _system_update_bootstrap_status();
+    registry_update();
+    drupal_get_schema(NULL, TRUE);
+    drupal_theme_rebuild();
+    entity_info_cache_clear();
+  }
+
+  /**
+   * Performs an unofficial module uninstall (in that no hooks are run and no
+   * dependencies are loaded or installed).
+   *
+   * @param string $module
+   *   The module to "uninstall."
+   */
+  protected function fakeModuleUninstall($module) {
+    db_update('system')
+      ->fields(array('status' => 0))
+      ->condition('type', 'module')
+      ->condition('name', $module)
+      ->execute();
+
+    system_list_reset();
+    module_list(TRUE);
+    module_implements('', FALSE, TRUE);
+    entity_info_cache_clear();
+    registry_update();
+    _system_update_bootstrap_status();
+    drupal_theme_rebuild();
+  }
+}
+
+
+/**
+ * End-to-end tests for Project Source: GitHub.
+ */
+class ProjectSrcGithubEndToEndTests extends ProjectSrcGithubBaseCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Project Source GitHub API implementation',
+      'description' => 'Tests the Project Source API implementation.',
+      'group' => 'Project Source',
+    );
+  }
+
+  public function testProjectSourceProjectXml() {
+    // Check for valid XML at 6.x.
+    $this->drupalGet('drupal/release-history/' . $this->org . '_module/6.x');
+    $headers = $this->drupalGetHeaders();
+    $this->assertEqual($headers['content-type'], 'text/xml; charset=utf-8', 'Found valid XML for 6.x.');
+    $this->validateProjectXml($this->xpath('//project'));
+    $this->assertNoFieldByXpath('//version', 'master', 'No release found for "master."');
+    $releases = $this->xpath('//release');
+    $this->assertTrue(count($releases) == 1, 'Found 1 release for 6.x.');
+    foreach ($releases as $release) {
+      $this->validateReleaseXml($release);
+    }
+
+    // Check for valid XML at 7.x.
+    $this->drupalGet('drupal/release-history/' . $this->org . '_module/7.x');
+    $headers = $this->drupalGetHeaders();
+    $this->assertEqual($headers['content-type'], 'text/xml; charset=utf-8', 'Found valid XML for 7.x.');
+    $this->validateProjectXml($this->xpath('//project'));
+    $this->assertNoFieldByXpath('//version', 'master', 'No release found for "master."');
+    $releases = $this->xpath('//release');
+    $this->assertTrue(count($releases) == 2, 'Found 2 releases for 7.x');
+    foreach ($releases as $release) {
+      $this->validateReleaseXml($release);
+    }
+
+    // Check that there are no results for 8.x.
+    $this->drupalGet('drupal/release-history/' . $this->org . '_module/8.x');
+    $headers = $this->drupalGetHeaders();
+    $this->assertEqual($headers['content-type'], 'text/xml; charset=utf-8', 'Found valid XML for 8.x.');
+    $this->assertNoFieldByXpath('//release', NULL, 'No releases found for 8.x.');
+    $text = 'No release history was found for the requested project (' . $this->org . '_module).';
+    $this->assertFieldByXPath('//error', $text, 'Found "no release history" messaging for 8.x');
+  }
+}
diff --git a/test/project_src_github_test_github_api/project_src_github_test_github_api.info b/test/project_src_github_test_github_api/project_src_github_test_github_api.info
new file mode 100644
index 0000000..08068b2
--- /dev/null
+++ b/test/project_src_github_test_github_api/project_src_github_test_github_api.info
@@ -0,0 +1,5 @@
+name = Project Source: GitHub - Mock GitHub API
+description = Mock GitHub API module for Project Source: GitHub. Do not enable.
+core = 7.x
+package = Testing
+hidden = TRUE
diff --git a/test/project_src_github_test_github_api/project_src_github_test_github_api.module b/test/project_src_github_test_github_api/project_src_github_test_github_api.module
new file mode 100644
index 0000000..97672ad
--- /dev/null
+++ b/test/project_src_github_test_github_api/project_src_github_test_github_api.module
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @file
+ * Classes and functions used to help test the Project Source: GitHub module.
+ */
+
+
+if (!function_exists('github_api_client')) {
+  /**
+   * Returns a mock GitHub API object, similar to what is returned by the GitHub
+   * API module.
+   *
+   * @return GithubApiMock
+   *   A custom mock GitHub API object we use for testing.
+   */
+  function github_api_client() {
+    return new GithubApiMock();
+  }
+}
+
+
+class GithubApiMock {
+
+  /**
+   * Returns an instance of the GithubApiMock object.
+   *
+   * @param string $type
+   *
+   * @return GithubApiMock
+   *   Rather than dealing with multiple mock objects, we set this object up to
+   *   return an instance of itself, which contains all methods we actually use.
+   */
+  public function api($type) {
+    return $this;
+  }
+
+  /**
+   * Returns an array that mimics a response from GitHub's API given a request
+   * for repositories for a given organization.
+   *
+   * @param string $organization
+   * @return array
+   *   An array representing all GitHub repos for a given organization.
+   */
+  public function repositories($organization) {
+    $module = $organization . '_module';
+    $repos[$module] = array(
+      'id' => 1234567,
+      'name' => $module,
+      'full_name' => $organization . '/' . $module,
+      'owner' => array(
+        'login' => $organization . '_user',
+      ),
+      'html_url' => 'http://example.com/' . $organization . '/' . $module,
+      'default_branch' => '7.x-2.x',
+    );
+
+    return $repos;
+  }
+
+  /**
+   * Returns an array that mimics a response from GitHub's API given a request
+   * for branches for a given author and repository.
+   *
+   * @param string $username
+   * @param string $repository
+   * @param string $branch
+   *
+   * @return array
+   *   An array representing all branches for a given repository.
+   */
+  public function branches($username, $repository, $branch = NULL) {
+    // Note we're making the branch name the key of the returned array. Although
+    // this does not match up with GitHub's response, we use it for testing.
+    $branches['7.x-1.x'] = array(
+      'name' => '7.x-1.x',
+      'commit' => array(
+        'sha' => '231ed864c57ce2c890230b52f958dd33c65e54f7',
+        'url' => 'http://api.example.com/repos/commits/etc/71',
+      ),
+    );
+    $branches['7.x-2.x'] = array(
+      'name' => '7.x-2.x',
+      'commit' => array(
+        'sha' => '65812be004d5b2701be8099c5886071a4909e775',
+        'url' => 'http://api.example.com/repos/commits/etc/72',
+      ),
+    );
+    $branches['6.x-1.x'] = array(
+      'name' => '6.x-1.x',
+      'commit' => array(
+        'sha' => '231ed864c57ce2c890230b52f958dd33c65e54f6',
+        'url' => 'http://api.example.com/repos/commits/etc/61',
+      ),
+    );
+    $branches['master'] = array(
+      'name' => 'master',
+      'commit' => array(
+        'sha' => '65812be004d5b2701be8099c5886071a4909e774',
+        'url' => 'http://api.example.com/repos/commits/etc/master',
+      ),
+    );
+
+    return $branches;
+  }
+
+  /**
+   * Returns an instance of the GithubApiMock object.
+   *
+   * @return GithubApiMock
+   *   Rather than dealing with multiple mock objects, we set this method up to
+   *   return an instance of itself. Commit methods are also located on this
+   *   object.
+   */
+  public function commits() {
+    return $this;
+  }
+
+  /**
+   * Returns an array that mimics a response from GitHub's API given a request
+   * for commit details for a given commit on a given repository.
+   *
+   * @param $username
+   * @param $repository
+   * @param $sha
+   *
+   * @return array
+   *   An array representing commit details for a given commitref for a given
+   *   repository.
+   */
+  public function show($username, $repository, $sha) {
+    return array(
+      'commit' => array(
+        'author' => array(
+          'date' => 'November 19, 1978',
+        ),
+      ),
+    );
+  }
+
+  /**
+   * Returns an array that mimics a response from GitHub's API given a request
+   * for release tags for a given repository.
+   *
+   * @param $username
+   * @param $repository
+   *
+   * @return array
+   *   An array representing release tag details for a given repository.
+   */
+  public function tags($username, $repository) {
+    return array();
+  }
+}
