diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..eab48cc
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending for every file
+# Indent with 4 spaces
+[php]
+end_of_line = lf
+indent_style = space
+indent_size = 4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e35df3f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+phpunit.xml
+composer.phar
+composer.lock
+vendor/
+build/
+.idea
+.DS_STORE
diff --git a/composer.json b/composer.json
index 924ee5c..286d286 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
     "name": "imsglobal/lti",
     "version" : "3.0.2",
     "description": "LTI Tool Provider Library",
-    "keywords": ["lti"],
+    "keywords": ["lti", "ims", "content-item", "edtech", "education", "lms"],
     "homepage": "https://www.imsglobal.org/lti",
     "type": "library",
     "license": "Apache-2.0",
@@ -19,5 +19,19 @@
         "psr-4": {
             "IMSGlobal\\LTI\\": "src/"
         }
+    },
+    "require-dev": {
+        "ext-curl": "*",
+        "phpunit/phpunit": "^5.6"
+    },
+    "autoload-dev":{
+        "psr-4": {
+            "IMSGlobal\\LTI\\Test\\": "tests/"
+        }
+    },
+    "scripts": {
+        "test": "phpunit",
+        "coverage": "phpunit --coverage-html=build/coverage",
+        "coverage-text": "phpunit --coverage-text"
     }
 }
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..0015ede
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,21 @@
+
+
+
+    
+        
+            tests/
+        
+    
+
+    
+        
+            src
+        
+    
+
+    
+        
+    
+
+
diff --git a/src/HTTP/Client.php b/src/HTTP/Client.php
new file mode 100644
index 0000000..b8af6d9
--- /dev/null
+++ b/src/HTTP/Client.php
@@ -0,0 +1,27 @@
+
+ * @copyright  IMS Global Learning Consortium Inc
+ * @date  2016
+ * @version 3.0.0
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
+ */
+interface Client
+{
+
+    /**
+     * Send the provided HTTPMessage and then updates it with the response data.
+     * 
+     * @param HTTPMessage $message The HTTP message to send
+     * @return bool If successful, returns true
+     */
+    public function send(HTTPMessage $message);
+
+}
diff --git a/src/HTTP/CurlClient.php b/src/HTTP/CurlClient.php
new file mode 100644
index 0000000..08d1dc2
--- /dev/null
+++ b/src/HTTP/CurlClient.php
@@ -0,0 +1,69 @@
+
+ * @copyright  IMS Global Learning Consortium Inc
+ * @date  2016
+ * @version 3.0.0
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
+ */
+class CurlClient implements Client
+{
+
+    /**
+     * @inheritdoc
+     */
+    public function send(HTTPMessage $message)
+    {
+        $message->ok = false;
+
+        $resp = '';
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $message->url);
+        if (!empty($message->requestHeaders)) {
+            curl_setopt($ch, CURLOPT_HTTPHEADER, $message->requestHeaders);
+        } else {
+            curl_setopt($ch, CURLOPT_HEADER, 0);
+        }
+        if ($message->method === 'POST') {
+            curl_setopt($ch, CURLOPT_POST, true);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, $message->request);
+        } else if ($message->method !== 'GET') {
+            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $message->method);
+            if (!is_null($message->request)) {
+                curl_setopt($ch, CURLOPT_POSTFIELDS, $message->request);
+            }
+        }
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLINFO_HEADER_OUT, true);
+        curl_setopt($ch, CURLOPT_HEADER, true);
+        $chResp = curl_exec($ch);
+        $message->ok = $chResp !== false;
+        if ($message->ok) {
+            $chResp = str_replace("\r\n", "\n", $chResp);
+            $chRespSplit = explode("\n\n", $chResp, 2);
+            if ((count($chRespSplit) > 1) && (substr($chRespSplit[1], 0, 5) === 'HTTP/')) {
+                $chRespSplit = explode("\n\n", $chRespSplit[1], 2);
+            }
+            $message->responseHeaders = $chRespSplit[0];
+            $resp = $chRespSplit[1];
+            $message->status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+            $message->ok = $message->status < 400;
+            if (!$message->ok) {
+                $message->error = curl_error($ch);
+            }
+        }
+        $message->requestHeaders = str_replace("\r\n", "\n", curl_getinfo($ch, CURLINFO_HEADER_OUT));
+        curl_close($ch);
+        $message->response = $resp;
+
+        return $message->ok;
+    }
+
+}
diff --git a/src/HTTP/StreamClient.php b/src/HTTP/StreamClient.php
new file mode 100644
index 0000000..7bd6f8e
--- /dev/null
+++ b/src/HTTP/StreamClient.php
@@ -0,0 +1,55 @@
+
+ * @copyright  IMS Global Learning Consortium Inc
+ * @date  2016
+ * @version 3.0.0
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
+ */
+class StreamClient implements Client
+{
+
+    /**
+     * @inheritdoc
+     */
+    public function send(HTTPMessage $message)
+    {
+        $message->ok = false;
+
+        // Prepare options for the HTTP context.
+        $opts = array(
+            'method' => $message->method,
+            'content' => $message->request
+        );
+        if (!empty($message->requestHeaders)) {
+            $opts['header'] = $message->requestHeaders;
+        }
+
+        // Send the request.
+        $http_response_header = null;
+        $context = stream_context_create(['http' => $opts]);
+        $stream = @fopen($message->url, 'rb', false, $context);
+        if ($stream) {
+            $message->response = @stream_get_contents($stream);
+            fclose($stream);
+        }
+
+        // Read the headers to get the status.
+        if ($http_response_header) {
+            $message->responseHeaders = implode("\n", $http_response_header);
+            $parts = explode(' ', $message->responseHeaders, 3);
+            $message->status = $parts[1];
+            $message->ok = $message->status < 400;
+        }
+
+        return $message->ok;
+    }
+
+}
diff --git a/src/HTTPMessage.php b/src/HTTPMessage.php
index 1fa2a9a..a2ae09b 100644
--- a/src/HTTPMessage.php
+++ b/src/HTTPMessage.php
@@ -2,8 +2,12 @@
 
 namespace IMSGlobal\LTI;
 
+use IMSGlobal\LTI\HTTP\Client as HTTPClient;
+use IMSGlobal\LTI\HTTP\CurlClient;
+use IMSGlobal\LTI\HTTP\StreamClient;
+
 /**
- * Class to represent an HTTP message
+ * Class to represent an HTTP message.
  *
  * @author  Stephen P Vickers 
  * @copyright  IMS Global Learning Consortium Inc
@@ -13,167 +17,114 @@
  */
 class HTTPMessage
 {
-
-/**
- * True if message was sent successfully.
- *
- * @var boolean $ok
- */
+    /**
+     * @var HTTPClient The client used to send the request.
+     */
+    private static $httpClient;
+
+    /**
+     * @var bool True if message was sent successfully.
+     */
     public $ok = false;
 
-/**
- * Request body.
- *
- * @var request $request
- */
+    /**
+     * @var string|null Request body.
+     */
     public $request = null;
 
-/**
- * Request headers.
- *
- * @var request_headers $requestHeaders
- */
-    public $requestHeaders = '';
+    /**
+     * @var array Request headers.
+     */
+    public $requestHeaders = [];
 
-/**
- * Response body.
- *
- * @var response $response
- */
+    /**
+     * @var string|null Response body.
+     */
     public $response = null;
 
-/**
- * Response headers.
- *
- * @var response_headers $responseHeaders
- */
+    /**
+     * @var string Response headers.
+     */
     public $responseHeaders = '';
 
-/**
- * Status of response (0 if undetermined).
- *
- * @var status $status
- */
+    /**
+     * @var int Status of response (0 if undetermined).
+     */
     public $status = 0;
 
-/**
- * Error message
- *
- * @var error $error
- */
+    /**
+     * @var string Error message
+     */
     public $error = '';
 
-/**
- * Request URL.
- *
- * @var url $url
- */
-    private $url = null;
+    /**
+     * @var string Request URL.
+     */
+    public $url = null;
+
+    /**
+     * @var string Request method.
+     */
+    public $method = null;
+
+    /**
+     * Allows you to set a custom HTTP client.
+     * 
+     * @param HTTPClient|null $httpClient The HTTP client to use for sending message.
+     */
+    public static function setHttpClient(HTTPClient $httpClient = null)
+    {
+        self::$httpClient = $httpClient;
+    }
 
-/**
- * Request method.
- *
- * @var method $method
- */
-    private $method = null;
+    /**
+     * Retrieves the HTTP client used for sending the message. Creates a default client if one is not set.
+     * 
+     * @return HTTPClient
+     */
+    public static function getHttpClient()
+    {
+        if (!self::$httpClient) {
+            // @codeCoverageIgnoreStart
+            if (function_exists('curl_init')) {
+                self::$httpClient =  new CurlClient();
+            } elseif (ini_get('allow_url_fopen')) {
+                self::$httpClient =  new StreamClient();
+            } else {
+                throw new \RuntimeException('Cannot create an HTTP client, because neither cURL or allow_url_fopen are enabled.');
+            }
+            // @codeCoverageIgnoreEnd
+        }
+        
+        return self::$httpClient;
+    }
 
-/**
- * Class constructor.
- *
- * @param string $url     URL to send request to
- * @param string $method  Request method to use (optional, default is GET)
- * @param mixed  $params  Associative array of parameter values to be passed or message body (optional, default is none)
- * @param string $header  Values to include in the request header (optional, default is none)
- */
+    /**
+     * Class constructor.
+     *
+     * @param string $url     URL to send request to
+     * @param string $method  Request method to use (optional, default is GET)
+     * @param array|string  $params  Associative array of parameter values to be passed or message body (optional, default is none)
+     * @param array|string $header  Values to include in the request header (optional, default is none)
+     */
     function __construct($url, $method = 'GET', $params = null, $header = null)
     {
-
         $this->url = $url;
         $this->method = strtoupper($method);
-        if (is_array($params)) {
-            $this->request = http_build_query($params);
-        } else {
-            $this->request = $params;
-        }
-        if (!empty($header)) {
+        $this->request = is_array($params) ? http_build_query($params) : $params;
+        if ($header && !is_array($header)) {
             $this->requestHeaders = explode("\n", $header);
         }
-
     }
 
-/**
- * Send the request to the target URL.
- *
- * @return boolean True if the request was successful
- */
+    /**
+     * Send the request to the target URL.
+     *
+     * @return boolean True if the request was successful
+     */
     public function send()
     {
-
-        $this->ok = false;
-// Try using curl if available
-        if (function_exists('curl_init')) {
-            $resp = '';
-            $ch = curl_init();
-            curl_setopt($ch, CURLOPT_URL, $this->url);
-            if (!empty($this->requestHeaders)) {
-                curl_setopt($ch, CURLOPT_HTTPHEADER, $this->requestHeaders);
-            } else {
-                curl_setopt($ch, CURLOPT_HEADER, 0);
-            }
-            if ($this->method === 'POST') {
-                curl_setopt($ch, CURLOPT_POST, true);
-                curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
-            } else if ($this->method !== 'GET') {
-                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
-                if (!is_null($this->request)) {
-                    curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
-                }
-            }
-            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-            curl_setopt($ch, CURLINFO_HEADER_OUT, true);
-            curl_setopt($ch, CURLOPT_HEADER, true);
-            //curl_setopt($ch, CURLOPT_SSLVERSION,3);
-            $chResp = curl_exec($ch);
-            $this->ok = $chResp !== false;
-            if ($this->ok) {
-                $chResp = str_replace("\r\n", "\n", $chResp);
-                $chRespSplit = explode("\n\n", $chResp, 2);
-                if ((count($chRespSplit) > 1) && (substr($chRespSplit[1], 0, 5) === 'HTTP/')) {
-                    $chRespSplit = explode("\n\n", $chRespSplit[1], 2);
-                }
-                $this->responseHeaders = $chRespSplit[0];
-                $resp = $chRespSplit[1];
-                $this->status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-                $this->ok = $this->status < 400;
-                if (!$this->ok) {
-                    $this->error = curl_error($ch);
-                }
-            }
-            $this->requestHeaders = str_replace("\r\n", "\n", curl_getinfo($ch, CURLINFO_HEADER_OUT));
-            curl_close($ch);
-            $this->response = $resp;
-        } else {
-// Try using fopen if curl was not available
-            $opts = array('method' => $this->method,
-                          'content' => $this->request
-                         );
-            if (!empty($this->requestHeaders)) {
-                $opts['header'] = $this->requestHeaders;
-            }
-            try {
-                $ctx = stream_context_create(array('http' => $opts));
-                $fp = @fopen($this->url, 'rb', false, $ctx);
-                if ($fp) {
-                    $resp = @stream_get_contents($fp);
-                    $this->ok = $resp !== false;
-                }
-            } catch (\Exception $e) {
-                $this->ok = false;
-            }
-        }
-
-        return $this->ok;
-
+        return self::getHttpClient()->send($this);
     }
 
 }
diff --git a/tests/HTTP/ClientTest.php b/tests/HTTP/ClientTest.php
new file mode 100644
index 0000000..d9d644b
--- /dev/null
+++ b/tests/HTTP/ClientTest.php
@@ -0,0 +1,108 @@
+server = new TestServer();
+        $this->server->start();
+        if (!$this->server->isRunning()) {
+            $this->markTestSkipped('Test server is not running.');
+        }
+    }
+
+    protected function tearDown()
+    {
+        parent::tearDown();
+        $this->server->stop();
+    }
+    
+    public function provideClient()
+    {
+        return [[new CurlClient], [new StreamClient]];
+    }
+
+    /**
+     * @param HttpClient $client
+     * @dataProvider provideClient
+     */
+    public function testCanSendPostMessage(HttpClient $client)
+    {
+        $url = $this->server->getUrl(200, TestServer::RETURN_INPUT);
+        $message = new HTTPMessage($url, 'POST', 'hello');
+        $client->send($message);
+        
+        $this->assertTrue($message->ok);
+        $this->assertEquals('hello', $message->response);
+    }
+
+    /**
+     * @param HttpClient $client
+     * @dataProvider provideClient
+     */
+    public function testCanSendUncommonMethods(HttpClient $client)
+    {
+        $url = $this->server->getUrl(200, TestServer::RETURN_INPUT);
+        $message = new HTTPMessage($url, 'PATCH', 'hello');
+        $client->send($message);
+
+        $this->assertTrue($message->ok);
+        $this->assertEquals('hello', $message->response);
+    }
+
+    /**
+     * @param HttpClient $client
+     * @dataProvider provideClient
+     */
+    public function testCanSendMessageWithHeaders(HttpClient $client)
+    {
+        $url = $this->server->getUrl(200, TestServer::RETURN_HEADER, 'Test');
+        $message = new HTTPMessage($url, 'POST', null, 'Test: foo');
+        $client->send($message);
+
+        $this->assertTrue($message->ok);
+        $this->assertEquals('foo', $message->response);
+    }
+
+    /**
+     * @param HttpClient $client
+     * @dataProvider provideClient
+     */
+    public function testCanHandleError(HttpClient $client)
+    {
+        $url = $this->server->getUrl(404);
+        $message = new HTTPMessage($url, 'GET');
+        $client->send($message);
+
+        $this->assertFalse($message->ok);
+        $this->assertEquals(404, $message->status);
+    }
+
+    public function testCanHandleResponsesWithRepeatedHeaderBlock()
+    {
+        $url = $this->server->getUrl(200, TestServer::RETURN_VALUE, "HTTP/1.1 200 OK\n\nsuccess");
+        $message = new HTTPMessage($url, 'GET');
+        $client = new CurlClient();
+        $client->send($message);
+
+        $this->assertTrue($message->ok);
+        $this->assertEquals('success', $message->response);
+    }
+}
diff --git a/tests/HTTP/TestServer.php b/tests/HTTP/TestServer.php
new file mode 100644
index 0000000..39689a6
--- /dev/null
+++ b/tests/HTTP/TestServer.php
@@ -0,0 +1,77 @@
+getPort();
+        $this->testServerPid = @exec("php -S localhost:{$port} -t {$dir} {$dir}/server.php &> /dev/null & echo $!");
+        
+        // Wait a little bit for the server to come online.
+        usleep(100000);
+    }
+
+    /**
+     * Determines if the server is running.
+     *
+     * @return bool
+     */
+    public function isRunning()
+    {
+        $ping = @file_get_contents($url = $this->getUrl(200, self::RETURN_VALUE, 'ping'));
+        return $ping === 'ping';
+    }
+
+    /**
+     * Gets the URL to the test server.
+     *
+     * @return string Test server's URL
+     */
+    public function getUrl($status = 200, $type = self::RETURN_NOTHING, $value = null)
+    {
+        $port = $this->getPort();
+        $query = http_build_query(compact('status', 'type', 'value'));
+
+        return "http://localhost:{$port}/?{$query}";
+    }
+
+    /**
+     *  Stop the test server, if it's running, and the PID is known.
+     */
+    public function stop()
+    {
+        if (is_numeric($this->testServerPid)) {
+            @exec("kill {$this->testServerPid}");
+        }
+    }
+
+    /**
+     * Get the test server port, as provided in phpunit.xml(.dist).
+     *
+     * @return int
+     */
+    private function getPort()
+    {
+        if (isset($_SERVER['TEST_SERVER_PORT']) && is_numeric($_SERVER['TEST_SERVER_PORT'])) {
+            return (int) $_SERVER['TEST_SERVER_PORT'];
+        } else {
+            throw new \RuntimeException('TEST_SERVER_PORT is not defined as a $_SERVER variable in your phpunit.xml');
+        }
+    }
+}
diff --git a/tests/HTTP/server.php b/tests/HTTP/server.php
new file mode 100644
index 0000000..a2c0313
--- /dev/null
+++ b/tests/HTTP/server.php
@@ -0,0 +1,18 @@
+createMock(Client::class);
+        HTTPMessage::setHttpClient($client);
+        
+        $this->assertInstanceOf(Client::class, HTTPMessage::getHttpClient());
+    }
+
+    public function testUsesCurlClientIfCurlIsAvailable()
+    {
+        $client = HTTPMessage::getHttpClient();
+        
+        $this->assertInstanceOf(CurlClient::class, $client);
+    }
+
+    public function testCanCreateAFormattedHttpMessage()
+    {
+        $message = new HTTPMessage(
+            'http://example.com',
+            'post',
+            ['a' => 1, 'b' => 2],
+            "Foo: abc\nBar: xyz"
+        );
+        
+        $this->assertEquals('http://example.com', $message->url);
+        $this->assertEquals('POST', $message->method);
+        $this->assertEquals('a=1&b=2', $message->request);
+        $this->assertInternalType('array', $message->requestHeaders);
+        $this->assertCount(2, $message->requestHeaders);
+    }
+
+    public function testCanSendAnHttpMessage()
+    {
+        // Create a message to send
+        $message = new HTTPMessage('http://example.com', 'POST');
+
+        // Create a mock client and configure to be used for sending HTTP messages
+        $client = $this->createMock(Client::class);
+        $client->expects($this->once())
+            ->method('send')
+            ->with($message)
+            ->willReturnCallback(function (HTTPMessage $message) {
+                return $message->ok = true;
+            });
+        HTTPMessage::setHttpClient($client);
+
+        // Send the message
+        $result = $message->send();
+
+        // Verify success
+        $this->assertTrue($result);
+        $this->assertTrue($message->ok);
+    }
+    
+    protected function setUp()
+    {
+        parent::setUp();
+        
+        // Reset the HTTP client
+        HTTPMessage::setHttpClient(null);
+    }
+
+    protected function tearDown()
+    {
+        parent::tearDown();
+
+        // Reset the HTTP client
+        HTTPMessage::setHttpClient(null);
+    }
+}