diff --git a/test/fixtures/files/test.png b/test/fixtures/files/test.png
new file mode 100644
index 0000000000000000000000000000000000000000..afbbb4f8b77c0826e00104e7a685a732184bb5f5
Binary files /dev/null and b/test/fixtures/files/test.png differ
diff --git a/test/fixtures/http_invalid_header.xml b/test/fixtures/http_invalid_header.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6ddfedc088de25082a8644c6d1da39d4bd0dff52
--- /dev/null
+++ b/test/fixtures/http_invalid_header.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<Error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
+  <Code>InvalidHeaderValue</Code>
+  <Message>The value for one of the HTTP headers is not in the correct format.\nRequestId:266be2bc-0001-0018-1256-d3bb92000000\nTime:2016-07-01T05:06:32.5922690Z</Message>
+  <HeaderName>Range</HeaderName>
+  <HeaderValue>bytes=0-512</HeaderValue>
+</Error>
diff --git a/test/support/fixtures.rb b/test/support/fixtures.rb
index 087df4ff6303896a89075e9517f9ffcb0a907dad..4398aa167fc5656597d06893c40233fe932cea6f 100644
--- a/test/support/fixtures.rb
+++ b/test/support/fixtures.rb
@@ -21,7 +21,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 #--------------------------------------------------------------------------
-require "pathname"
+require "azure/core/http/retry_policy"
 
 Fixtures = Hash.new do |hash, fixture|
   if path = Fixtures.xml?(fixture)
@@ -49,3 +49,48 @@ end
 def Fixtures.json?(fixture)
   file?("#{fixture}.json")
 end
+
+module Azure
+  module Core
+    Fixtures = Hash.new do |hash, fixture|
+      if path = Fixtures.xml?(fixture)
+        hash[fixture] = path.read
+      elsif path = Fixtures.file?(fixture)
+        hash[fixture] = path
+      end
+    end
+    def Fixtures.root
+      Pathname("../../fixtures").expand_path(__FILE__)
+    end
+    def Fixtures.file?(fixture)
+      path = root.join(fixture)
+      path.file? && path
+    end
+    def Fixtures.xml?(fixture)
+      file?("#{fixture}.xml")
+    end
+    
+    class FixtureRetryPolicy < Azure::Core::Http::RetryPolicy
+      def initialize
+        super &:should_retry?
+      end
+      def should_retry?(response, retry_data)
+        retry_data[:error].inspect.include?('Error: Retry')
+      end
+    end
+
+    class NewUriRetryPolicy < Azure::Core::Http::RetryPolicy
+      def initialize
+        @count = 1
+        super &:should_retry?
+      end
+
+      def should_retry?(response, retry_data)
+        retry_data[:uri] = URI.parse "http://bar.com"
+        @count = @count - 1
+        @count >= 0
+      end
+    end
+
+  end
+end
diff --git a/test/unit/core/auth/shared_key_lite_test.rb b/test/unit/core/auth/shared_key_lite_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..75a49e94115cdb71a1fddbfda185d324e5539173
--- /dev/null
+++ b/test/unit/core/auth/shared_key_lite_test.rb
@@ -0,0 +1,51 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require 'test_helper'
+require 'azure/core/auth/shared_key_lite'
+
+describe Azure::Core::Auth::SharedKeyLite do
+  subject { Azure::Core::Auth::SharedKeyLite.new 'account-name', 'YWNjZXNzLWtleQ==' }
+
+  let(:verb) { 'POST' }
+  let(:uri) { URI.parse 'http://dummy.uri/resource' }
+  let(:headers) do
+    {
+      'Content-MD5' => 'foo',
+      'Content-Type' => 'foo',
+      'Date' => 'foo'
+    }
+  end
+  let(:headers_without_date) {
+    headers_without_date = headers.clone
+    headers_without_date.delete 'Date'
+    headers_without_date
+  }
+
+  describe 'sign' do
+    it 'creates a signature from the provided HTTP method, uri, and reduced set of standard headers' do
+      subject.sign(verb, uri, headers).must_equal 'account-name:vVFnj/+27JFABZgpt5H8g/JVU2HuWFnjv5aeUIxQvBE='
+    end
+
+    it 'ignores standard headers other than Content-MD5, Content-Type, and Date' do
+      subject.sign(verb, uri, headers.merge({'Content-Encoding' => 'foo'})).must_equal 'account-name:vVFnj/+27JFABZgpt5H8g/JVU2HuWFnjv5aeUIxQvBE='
+    end
+
+    it 'throws IndexError when there is no Date header' do
+      assert_raises IndexError do
+        subject.sign(verb, uri, headers_without_date)
+      end
+    end
+  end
+end
diff --git a/test/unit/core/auth/shared_key_test.rb b/test/unit/core/auth/shared_key_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0cd86466e8d3f5fd3a2a85164859ec3a67fc16c2
--- /dev/null
+++ b/test/unit/core/auth/shared_key_test.rb
@@ -0,0 +1,59 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require 'test_helper'
+require 'azure/core/auth/shared_key'
+
+describe Azure::Core::Auth::SharedKey do
+  subject { Azure::Core::Auth::SharedKey.new 'account-name', 'YWNjZXNzLWtleQ==' }
+
+  let(:verb) { 'POST' }
+  let(:uri) { URI.parse 'http://dummy.uri/resource' }
+  let(:headers) do
+    {
+      'Content-Encoding' => 'foo',
+      'Content-Language' => 'foo',
+      'Content-Length' => 'foo',
+      'Content-MD5' => 'foo',
+      'Content-Type' => 'foo',
+      'Date' => 'foo',
+      'If-Modified-Since' => 'foo',
+      'If-Match' => 'foo',
+      'If-None-Match' => 'foo',
+      'If-Unmodified-Since' => 'foo',
+      'Range' => 'foo',
+      'x-ms-ImATeapot' => 'teapot',
+      'x-ms-ShortAndStout' => 'True',
+      'x-ms-reserve-spaces' => 'two  speces'
+    }
+  end
+
+  describe 'sign' do
+    it 'creates a signature from the provided HTTP method, uri, and a specific set of standard headers' do
+      subject.sign(verb, uri, headers).must_equal 'account-name:TVilUAfUwtHIVp+eonglFDXfS5r0/OE0/vVX3GHcaxU='
+    end
+  end
+
+  describe 'canonicalized_headers' do
+    it 'creates a canonicalized header string' do
+      subject.canonicalized_headers(headers).must_equal "x-ms-imateapot:teapot\nx-ms-reserve-spaces:two  speces\nx-ms-shortandstout:True"
+    end
+  end
+
+  describe 'canonicalized_resource' do
+    it 'creates a canonicalized resource string' do
+      subject.canonicalized_resource(uri).must_equal '/account-name/resource'
+    end
+  end
+end
diff --git a/test/unit/core/auth/signer_test.rb b/test/unit/core/auth/signer_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..917194b131e35924a1d8cb1b1de67564732ca217
--- /dev/null
+++ b/test/unit/core/auth/signer_test.rb
@@ -0,0 +1,30 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require "test_helper"
+require "azure/core/auth/signer"
+
+describe Azure::Core::Auth::Signer do
+  subject { Azure::Core::Auth::Signer.new "YWNjZXNzLWtleQ==" }
+
+  it "decodes the base64 encoded access_key" do
+    subject.access_key.must_equal "access-key"
+  end
+
+  describe "sign" do
+    it "creates a signature for the body, as a base64 encoded string, which represents a HMAC hash using the access_key" do
+      subject.sign("body").must_equal "iuUxVhs1E7PeSNx/90ViyJNO24160qYpoWeCcOsnMoM="
+    end
+  end
+end
diff --git a/test/unit/core/filter_service_test.rb b/test/unit/core/filter_service_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d16b222019a800a4075904095685de562a6b869a
--- /dev/null
+++ b/test/unit/core/filter_service_test.rb
@@ -0,0 +1,41 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require 'test_helper'
+require 'azure/core'
+require "azure/core/http/debug_filter"
+require "azure/core/http/retry_policy"
+
+describe 'Azure core service' do
+  subject do
+    Azure::Core::FilteredService.new
+  end
+
+  it 'works with default' do
+    subject.filters.count.must_equal 0
+  end
+
+  it 'works with a debug filter' do
+    service = Azure::Core::FilteredService.new
+    service.with_filter Azure::Core::Http::DebugFilter.new
+    service.filters.count.must_equal 1
+  end
+
+  it 'works with retry policy filter' do
+    service = Azure::Core::FilteredService.new
+    service.with_filter Azure::Core::Http::DebugFilter.new
+    service.with_filter Azure::Core::Http::RetryPolicy.new
+    service.filters.count.must_equal 2
+  end
+end
diff --git a/test/unit/core/http/http_error_test.rb b/test/unit/core/http/http_error_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..49b5040b2d186de6476f3761def0e8e639e3092d
--- /dev/null
+++ b/test/unit/core/http/http_error_test.rb
@@ -0,0 +1,113 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require 'test_helper'
+require 'azure/core/http/http_error'
+
+describe Azure::Core::Http::HTTPError do
+  let :http_response do
+    stub(body: Azure::Core::Fixtures[:http_error], status_code: 409, uri: 'http://dummy.uri', headers: { 'Content-Type' => 'application/atom+xml' })
+  end
+
+  subject do
+    Azure::Core::Http::HTTPError.new(http_response)
+  end
+
+  it 'is an instance of Azure::Core::Error' do
+    subject.must_be_kind_of Azure::Core::Error
+  end
+
+  it 'lets us see the original uri' do
+    subject.uri.must_equal 'http://dummy.uri'
+  end
+
+  it "lets us see the errors'status code" do
+    subject.status_code.must_equal 409
+  end
+
+  it "lets us see the error's type" do
+    subject.type.must_equal 'TableAlreadyExists'
+  end
+
+  it "lets us see the error's description" do
+    subject.description.must_equal 'The table specified already exists.'
+  end
+
+  it 'generates an error message that wraps both the type and description' do
+    subject.message.must_equal 'TableAlreadyExists (409): The table specified already exists.'
+  end
+
+  describe 'with invalid http_response body' do
+    let :http_response do
+      stub(:body => "\r\nInvalid request\r\n", :status_code => 409, :uri => 'http://dummy.uri', headers: {})
+    end
+
+    it 'sets the type to unknown if the response body is not an XML' do
+      subject.type.must_equal 'Unknown'
+      subject.description.must_equal 'Invalid request'
+    end
+  end
+
+  describe 'with invalid headers' do
+    let :http_response do
+      stub(body: Azure::Core::Fixtures[:http_invalid_header], status_code: 400, uri: 'http://dummy.uri', headers: { 'Content-Type' => 'application/atom+xml'})
+    end
+
+    it 'sets the invalid header in the error details' do
+      subject.status_code.must_equal 400
+      subject.type.must_equal 'InvalidHeaderValue'
+      subject.description.must_include 'The value for one of the HTTP headers is not in the correct format'
+      subject.header.must_equal 'Range'
+      subject.header_value.must_equal 'bytes=0-512'
+    end
+  end
+
+  describe 'with JSON payload' do
+    let :http_response do
+      body = "{\"odata.error\":{\"code\":\"ErrorCode\",\"message\":{\"lang\":\"en-US\",\"value\":\"ErrorDescription\"}}}"
+      stub(body: body, status_code: 400, uri: 'http://dummy.uri', headers: { 'Content-Type' => 'application/json' })
+    end
+
+    it 'parse error response with JSON payload' do
+      subject.status_code.must_equal 400
+      subject.type.must_equal 'ErrorCode'
+      subject.description.must_include 'ErrorDescription'
+    end
+  end
+
+  describe 'with unknown payload' do
+    let :http_response do
+      body = 'Unknown Payload Format with Unknown Error Description'
+      stub(body: body, status_code: 400, uri: 'http://dummy.uri', headers: {})
+    end
+
+    it 'parse error response with JSON payload' do
+      subject.status_code.must_equal 400
+      subject.type.must_equal 'Unknown'
+      subject.description.must_include 'Error Description'
+    end
+  end
+
+  describe 'with no response body' do
+    let :http_response do
+      body = ''
+      stub(body: body, status_code: 404, uri: 'http://dummy.uri', headers: {}, reason_phrase: 'dummy reason')
+    end
+
+    it 'message has value assigned from reason_phrase' do
+      subject.status_code.must_equal 404
+      subject.message.must_equal 'Unknown (404): dummy reason'
+    end
+  end
+end
diff --git a/test/unit/core/http/http_request_test.rb b/test/unit/core/http/http_request_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fa781fe387da0616501801ee87d543ad6a1ffac9
--- /dev/null
+++ b/test/unit/core/http/http_request_test.rb
@@ -0,0 +1,201 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require 'test_helper'
+require 'azure/core/http/http_request'
+
+describe Azure::Core::Http::HttpRequest do
+  let(:uri) { URI('http://example.com') }
+
+  describe ' default_headers ' do
+    subject do
+      Azure::Core::Http::HttpRequest.new(:get, uri, body: nil, current_time: 'Thu, 04 Oct 2012 06:38:27 GMT')
+    end
+
+    it 'sets the x-ms-date header to the current_time' do
+      subject.headers['x-ms-date'] = 'Thu, 04 Oct 2012 06:38:27 GMT'
+    end
+
+    it 'sets the x-ms-version header to the current API version' do
+      subject.headers['x-ms-version'] = '2011-08-18'
+    end
+
+    it 'sets the DataServiceVersion header to the current API version' do
+      subject.headers['DataServiceVersion'] = '1.0;NetFx'
+    end
+
+    it 'sets the MaxDataServiceVersion header to the current max` API version' do
+      subject.headers['MaxDataServiceVersion'] = '2.0;NetFx'
+    end
+  end
+
+  describe 'when passed custom headers' do
+    subject do
+      Azure::Core::Http::HttpRequest.new(:get,
+                                         uri,
+                                         body: nil,
+                                         headers: {
+                                             'blah' => 'something',
+                                             'x-ms-version' => '123'
+                                         })
+    end
+
+    it 'should have overridden the value of x-ms-version' do
+      subject.headers['x-ms-version'].must_equal '123'
+    end
+
+    it 'should have added in the blah = something header' do
+      subject.headers['blah'].must_equal 'something'
+    end
+
+  end
+
+  describe ' when passed a body ' do
+    describe " of type IO" do
+      subject do
+        file = File.open(File.expand_path("../../../../fixtures/files/test.png", __FILE__))
+        Azure::Core::Http::HttpRequest.new(:post, uri, body: file)
+      end
+
+      it 'sets the default Content-Type header' do
+        subject.headers['Content-Type'].must_equal 'application/atom+xml; charset=utf-8'
+      end
+
+      it 'sets the Content-Length header' do
+        subject.headers['Content-Length'].must_equal '4054'
+      end
+
+      it 'sets the Content-MD5 header to a Base64 encoded representation of the MD5 hash of the body' do
+        subject.headers['Content-MD5'].must_equal 'nxTCAVCgA9fOTeV8KY8Pug=='
+      end
+    end
+
+    describe 'of type Tempfile' do
+      subject do
+        tempfile = Tempfile.open('azure')
+        file = File.open(File.expand_path('../../../../fixtures/files/test.png', __FILE__))
+        IO.copy_stream(file, tempfile)
+
+        Azure::Core::Http::HttpRequest.new(:post, uri, body: tempfile)
+      end
+
+      it 'sets the default Content-Type header' do
+        subject.headers['Content-Type'].must_equal 'application/atom+xml; charset=utf-8'
+      end
+
+      it 'sets the Content-Length header' do
+        subject.headers['Content-Length'].must_equal '4054'
+      end
+
+      it 'sets the Content-MD5 header to a Base64 encoded representation of the MD5 hash of the body' do
+        subject.headers['Content-MD5'].must_equal 'nxTCAVCgA9fOTeV8KY8Pug=='
+      end
+    end
+
+    describe ' of type StringIO' do
+      subject do
+        Azure::Core::Http::HttpRequest.new(:post, uri, body: StringIO.new('<body/>'))
+      end
+
+      it 'sets the default Content-Type header' do
+        subject.headers['Content-Type'].must_equal 'application/atom+xml; charset=utf-8'
+      end
+
+      it 'sets the Content-Length header' do
+        subject.headers['Content-Length'].must_equal '7'
+      end
+
+      it 'sets the Content-MD5 header to a Base64 encoded representation of the MD5 hash of the body' do
+        subject.headers['Content-MD5'].must_equal 'PNeJy7qyzV4XUoBBHkVu0g=='
+      end
+    end
+
+
+    describe ' of type String' do
+      subject do
+        Azure::Core::Http::HttpRequest.new(:post, uri, body: '<body/>')
+      end
+
+      it 'sets the default Content-Type header' do
+        subject.headers['Content-Type'].must_equal 'application/atom+xml; charset=utf-8'
+      end
+
+      it 'sets the Content-Length header' do
+        subject.headers['Content-Length'].must_equal '7'
+      end
+
+      it 'sets the Content-MD5 header to a Base64 encoded representation of the MD5 hash of the body' do
+        subject.headers['Content-MD5'].must_equal 'PNeJy7qyzV4XUoBBHkVu0g=='
+      end
+    end
+  end
+
+  describe ' when the body is nil ' do
+    subject do
+      Azure::Core::Http::HttpRequest.new(:get, uri)
+    end
+
+    it 'leaves the Content-Type, Content-Length, and Content-MD5 headers blank' do
+      subject.headers['Content-Length'].must_equal '0'
+      subject.headers['Content-MD5'].must_be_nil
+    end
+  end
+
+  describe '#call' do
+
+    let(:mock_conn) do
+      conn = mock
+      conn.expects(:run_request, [uri, nil, nil]).returns(mock_res)
+      conn
+    end
+
+    subject do
+      sub = Azure::Core::Http::HttpRequest.new(:get, uri)
+      sub.expects(:http_setup).returns(mock_conn)
+      sub
+    end
+
+    describe 'on success' do
+      let(:body) { '</body>' }
+
+      let(:mock_res) do
+        res = mock
+        res.expects(:success?).returns(true)
+        res.expects(:body).returns(body)
+        res
+      end
+
+      it 'should return a response' do
+        subject.call.body.must_equal(body)
+      end
+    end
+
+    describe 'on failure' do
+      let(:body) { 'OH NO!!' }
+
+      let(:mock_res) do
+        res = mock
+        res.expects(:success?).returns(false).at_least_once
+        res.expects(:status).returns(401).at_least_once
+        res.expects(:body).returns(body).at_least_once
+        res.expects(:headers).returns({}).at_least_once
+        res
+      end
+
+      it 'should return a response' do
+       -> { subject.call }.must_raise(Azure::Core::Http::HTTPError)
+      end
+    end
+  end
+end
diff --git a/test/unit/core/http/http_response_test.rb b/test/unit/core/http/http_response_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..95a127db054d399a049d4ce00f9da0f4bdefbfa5
--- /dev/null
+++ b/test/unit/core/http/http_response_test.rb
@@ -0,0 +1,20 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require 'test_helper'
+require 'azure/core/http/http_response'
+
+describe Azure::Core::Http::HttpResponse do
+  # TODO: fill in with better tests.
+end
diff --git a/test/unit/core/service_test.rb b/test/unit/core/service_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2f706c9c96978fcd5f43452b1851cb1a143654ca
--- /dev/null
+++ b/test/unit/core/service_test.rb
@@ -0,0 +1,73 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require 'test_helper'
+require 'azure/core'
+
+describe 'Azure core service' do
+  subject do
+    Azure::Core::Service.new
+  end
+
+  it 'generate_uri should return URI instance' do
+    subject.host = 'http://dumyhost.uri'
+    subject.generate_uri.must_be_kind_of ::URI
+    subject.generate_uri.to_s.must_equal 'http://dumyhost.uri/'
+  end
+
+  it 'generate_uri should add path to the url' do
+    subject.generate_uri('resource/entity/').path.must_equal '/resource/entity/'
+  end
+
+  it 'generate_uri should correctly join the path if host url contained a path' do
+    subject.host = 'http://dummy.uri/host/path'
+    subject.generate_uri('resource/entity/').path.must_equal '/host/path/resource/entity/'
+  end
+
+  it 'generate_uri should encode the keys' do
+    subject.generate_uri('', {'key !' => 'value'}).query.must_include 'key+%21=value'
+  end
+
+  it 'generate_uri should encode the values' do
+    subject.generate_uri('', {'key' => 'value !'}).query.must_include 'key=value+%21'
+  end
+
+  it 'generate_uri should set query string to the encoded result' do
+    subject.generate_uri('', {'key' => 'value !', 'key !' => 'value'}).query.must_equal 'key=value+%21&key+%21=value'
+  end
+
+  it 'generate_uri should override the default timeout' do
+    subject.generate_uri('', {'timeout' => 45}).query.must_equal 'timeout=45'
+  end
+
+  it 'generate_uri should not include any query parameters' do
+    subject.generate_uri('', nil).query.must_be_nil
+  end
+
+  it 'generate_uri should not re-encode path with spaces' do
+    subject.host = 'http://dumyhost.uri'
+    encoded_path = 'blob%20name%20with%20spaces'
+    uri = subject.generate_uri(encoded_path, nil)
+    uri.host.must_equal 'dumyhost.uri'
+    uri.path.must_equal '/blob%20name%20with%20spaces'
+  end
+
+  it 'generate_uri should not re-encode path with special characters' do
+    subject.host = 'http://dumyhost.uri'
+    encoded_path = 'host/path/%D1%84%D0%B1%D0%B0%D1%84.jpg'
+    uri = subject.generate_uri(encoded_path, nil)
+    uri.host.must_equal 'dumyhost.uri'
+    uri.path.must_equal '/host/path/%D1%84%D0%B1%D0%B0%D1%84.jpg'
+  end
+end
diff --git a/test/unit/core/utility_test.rb b/test/unit/core/utility_test.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a8f77a39a682a7d0f23295525e41bebb692e1560
--- /dev/null
+++ b/test/unit/core/utility_test.rb
@@ -0,0 +1,123 @@
+#-------------------------------------------------------------------------
+# # Copyright (c) Microsoft and contributors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#--------------------------------------------------------------------------
+require 'test_helper'
+require 'azure/core/utility'
+
+describe Azure::Core::Logger do
+  subject { Azure::Core::Logger }
+  let(:msg) { "message" }
+
+  after {
+    subject.initialize_external_logger(nil)
+  }
+
+  describe "Log without external logger" do
+    before {
+      subject.initialize_external_logger(nil)
+    }
+
+    it "#info" do
+      out, err = capture_io { subject.info(msg) }
+      assert_equal("\e[37m\e[1m" + msg + "\e[0m\e[0m\n", out)
+    end
+
+    it "#error_with_exit" do
+      out, err = capture_io do
+        error = assert_raises(RuntimeError) do
+          subject.error_with_exit(msg)
+        end
+        assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m", error.message)
+      end
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m\n", out)
+    end
+
+    it "#warn" do
+      out, err = capture_io do
+        warn = subject.warn(msg)
+        assert_equal(msg, warn)
+      end
+      assert_equal("\e[33m" + msg + "\e[0m\n", out)
+    end
+
+    it "#error" do
+      out, err = capture_io do
+        error = subject.error(msg)
+        assert_equal(msg, error)
+      end
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m\n", out)
+    end
+
+    it "#exception_message" do
+      out, err = capture_io do
+        exception = assert_raises(RuntimeError) do
+          subject.exception_message(msg)
+        end
+        assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m", exception.message)
+      end
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m\n", out)
+    end
+
+    it "#success" do
+      out, err = capture_io { subject.success(msg) }
+      assert_equal("\e[32m" + msg + "\n\e[0m", out)
+    end
+  end
+
+  describe "Log with external logger" do
+    let(:fake_output) { StringIO.new }
+
+    before {
+      subject.initialize_external_logger(Logger.new(fake_output))
+    }
+
+    it "#info" do
+      subject.info(msg)
+      assert_match(/INFO -- : #{msg}\n/, fake_output.string)
+    end
+
+    it "#error_with_exit" do
+      error = assert_raises(RuntimeError) do
+        subject.error_with_exit(msg)
+      end
+      assert_match(/ERROR -- : #{msg}\n/, fake_output.string)
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m", error.message)
+    end
+
+    it "#warn" do
+      warn = subject.warn(msg)
+      assert_match(/WARN -- : #{msg}\n/, fake_output.string)
+      assert_equal(msg, warn)
+    end
+
+    it "#error" do
+      error = subject.error(msg)
+      assert_match(/ERROR -- : #{msg}\n/, fake_output.string)
+      assert_equal(msg, error)
+    end
+
+    it "#exception_message" do
+      exception = assert_raises(RuntimeError) do
+        subject.exception_message(msg)
+      end
+      assert_match(/WARN -- : #{msg}\n/, fake_output.string)
+      assert_equal("\e[31m\e[1m" + msg + "\e[0m\e[0m", exception.message)
+    end
+
+    it "#success" do
+      subject.success(msg)
+      assert_match(/INFO -- : #{msg}\n/, fake_output.string)
+    end
+  end
+end
\ No newline at end of file