Add api_core package (#4210)

* Add api_core package

* Address review comments
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/unit/__init__.py
diff --git a/tests/unit/future/__init__.py b/tests/unit/future/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/unit/future/__init__.py
diff --git a/tests/unit/future/test__helpers.py b/tests/unit/future/test__helpers.py
new file mode 100644
index 0000000..660d23a
--- /dev/null
+++ b/tests/unit/future/test__helpers.py
@@ -0,0 +1,37 @@
+# Copyright 2017, Google Inc.
+#
+# 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.
+
+import mock
+
+from google.api_core.future import _helpers
+
+
+@mock.patch('threading.Thread', autospec=True)
+def test_start_deamon_thread(unused_thread):
+    deamon_thread = _helpers.start_daemon_thread(target=mock.sentinel.target)
+    assert deamon_thread.daemon is True
+
+
+def test_safe_invoke_callback():
+    callback = mock.Mock(spec=['__call__'], return_value=42)
+    result = _helpers.safe_invoke_callback(callback, 'a', b='c')
+    assert result == 42
+    callback.assert_called_once_with('a', b='c')
+
+
+def test_safe_invoke_callback_exception():
+    callback = mock.Mock(spec=['__call__'], side_effect=ValueError())
+    result = _helpers.safe_invoke_callback(callback, 'a', b='c')
+    assert result is None
+    callback.assert_called_once_with('a', b='c')
diff --git a/tests/unit/future/test_polling.py b/tests/unit/future/test_polling.py
new file mode 100644
index 0000000..7ad9aee
--- /dev/null
+++ b/tests/unit/future/test_polling.py
@@ -0,0 +1,157 @@
+# Copyright 2017, Google Inc.
+#
+# 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.
+
+import concurrent.futures
+import threading
+import time
+
+import mock
+import pytest
+
+from google.api_core.future import polling
+
+
+class PollingFutureImpl(polling.PollingFuture):
+    def done(self):
+        return False
+
+    def cancel(self):
+        return True
+
+    def cancelled(self):
+        return False
+
+    def running(self):
+        return True
+
+
+def test_polling_future_constructor():
+    future = PollingFutureImpl()
+    assert not future.done()
+    assert not future.cancelled()
+    assert future.running()
+    assert future.cancel()
+
+
+def test_set_result():
+    future = PollingFutureImpl()
+    callback = mock.Mock()
+
+    future.set_result(1)
+
+    assert future.result() == 1
+    future.add_done_callback(callback)
+    callback.assert_called_once_with(future)
+
+
+def test_set_exception():
+    future = PollingFutureImpl()
+    exception = ValueError('meep')
+
+    future.set_exception(exception)
+
+    assert future.exception() == exception
+    with pytest.raises(ValueError):
+        future.result()
+
+    callback = mock.Mock()
+    future.add_done_callback(callback)
+    callback.assert_called_once_with(future)
+
+
+def test_invoke_callback_exception():
+    future = PollingFutureImplWithPoll()
+    future.set_result(42)
+
+    # This should not raise, despite the callback causing an exception.
+    callback = mock.Mock(side_effect=ValueError)
+    future.add_done_callback(callback)
+    callback.assert_called_once_with(future)
+
+
+class PollingFutureImplWithPoll(PollingFutureImpl):
+    def __init__(self):
+        super(PollingFutureImplWithPoll, self).__init__()
+        self.poll_count = 0
+        self.event = threading.Event()
+
+    def done(self):
+        self.poll_count += 1
+        self.event.wait()
+        self.set_result(42)
+        return True
+
+
+def test_result_with_polling():
+    future = PollingFutureImplWithPoll()
+
+    future.event.set()
+    result = future.result()
+
+    assert result == 42
+    assert future.poll_count == 1
+    # Repeated calls should not cause additional polling
+    assert future.result() == result
+    assert future.poll_count == 1
+
+
+class PollingFutureImplTimeout(PollingFutureImplWithPoll):
+    def done(self):
+        time.sleep(1)
+        return False
+
+
+def test_result_timeout():
+    future = PollingFutureImplTimeout()
+    with pytest.raises(concurrent.futures.TimeoutError):
+        future.result(timeout=1)
+
+
+def test_callback_background_thread():
+    future = PollingFutureImplWithPoll()
+    callback = mock.Mock()
+
+    future.add_done_callback(callback)
+
+    assert future._polling_thread is not None
+
+    # Give the thread a second to poll
+    time.sleep(1)
+    assert future.poll_count == 1
+
+    future.event.set()
+    future._polling_thread.join()
+
+    callback.assert_called_once_with(future)
+
+
+def test_double_callback_background_thread():
+    future = PollingFutureImplWithPoll()
+    callback = mock.Mock()
+    callback2 = mock.Mock()
+
+    future.add_done_callback(callback)
+    current_thread = future._polling_thread
+    assert current_thread is not None
+
+    # only one polling thread should be created.
+    future.add_done_callback(callback2)
+    assert future._polling_thread is current_thread
+
+    future.event.set()
+    future._polling_thread.join()
+
+    assert future.poll_count == 1
+    callback.assert_called_once_with(future)
+    callback2.assert_called_once_with(future)
diff --git a/tests/unit/gapic/test_config.py b/tests/unit/gapic/test_config.py
new file mode 100644
index 0000000..75a6e1c
--- /dev/null
+++ b/tests/unit/gapic/test_config.py
@@ -0,0 +1,89 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+from google.api_core import exceptions
+from google.api_core.gapic_v1 import config
+
+
+INTERFACE_CONFIG = {
+    'retry_codes': {
+        'idempotent': ['DEADLINE_EXCEEDED', 'UNAVAILABLE'],
+        'other': ['FAILED_PRECONDITION'],
+        'non_idempotent': []
+    },
+    'retry_params': {
+        'default': {
+            'initial_retry_delay_millis': 1000,
+            'retry_delay_multiplier': 2.5,
+            'max_retry_delay_millis': 120000,
+            'initial_rpc_timeout_millis': 120000,
+            'rpc_timeout_multiplier': 1.0,
+            'max_rpc_timeout_millis': 120000,
+            'total_timeout_millis': 600000
+        },
+        'other': {
+            'initial_retry_delay_millis': 1000,
+            'retry_delay_multiplier': 1,
+            'max_retry_delay_millis': 1000,
+            'initial_rpc_timeout_millis': 1000,
+            'rpc_timeout_multiplier': 1,
+            'max_rpc_timeout_millis': 1000,
+            'total_timeout_millis': 1000
+        },
+    },
+    'methods': {
+        'AnnotateVideo': {
+            'timeout_millis': 60000,
+            'retry_codes_name': 'idempotent',
+            'retry_params_name': 'default'
+        },
+        'Other': {
+            'timeout_millis': 60000,
+            'retry_codes_name': 'other',
+            'retry_params_name': 'other'
+        },
+        'Plain': {
+            'timeout_millis': 30000
+        }
+    }
+}
+
+
+def test_create_method_configs():
+    method_configs = config.parse_method_configs(INTERFACE_CONFIG)
+
+    retry, timeout = method_configs['AnnotateVideo']
+    assert retry._predicate(exceptions.DeadlineExceeded(None))
+    assert retry._predicate(exceptions.ServiceUnavailable(None))
+    assert retry._initial == 1.0
+    assert retry._multiplier == 2.5
+    assert retry._maximum == 120.0
+    assert retry._deadline == 600.0
+    assert timeout._initial == 120.0
+    assert timeout._multiplier == 1.0
+    assert timeout._maximum == 120.0
+
+    retry, timeout = method_configs['Other']
+    assert retry._predicate(exceptions.FailedPrecondition(None))
+    assert retry._initial == 1.0
+    assert retry._multiplier == 1.0
+    assert retry._maximum == 1.0
+    assert retry._deadline == 1.0
+    assert timeout._initial == 1.0
+    assert timeout._multiplier == 1.0
+    assert timeout._maximum == 1.0
+
+    retry, timeout = method_configs['Plain']
+    assert retry is None
+    assert timeout._timeout == 30.0
diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py
new file mode 100644
index 0000000..35ac144
--- /dev/null
+++ b/tests/unit/gapic/test_method.py
@@ -0,0 +1,226 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+import datetime
+
+import mock
+
+from google.api_core import exceptions
+from google.api_core import retry
+from google.api_core import timeout
+import google.api_core.gapic_v1.method
+import google.api_core.page_iterator
+
+
+def _utcnow_monotonic():
+    curr_value = datetime.datetime.min
+    delta = datetime.timedelta(seconds=0.5)
+    while True:
+        yield curr_value
+        curr_value += delta
+
+
+def test_wrap_method_basic():
+    method = mock.Mock(spec=['__call__'], return_value=42)
+
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+        method, metadata=None)
+
+    result = wrapped_method(1, 2, meep='moop')
+
+    assert result == 42
+    method.assert_called_once_with(1, 2, meep='moop')
+
+
+def test_wrap_method_with_default_metadata():
+    method = mock.Mock(spec=['__call__'])
+
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)
+
+    wrapped_method(1, 2, meep='moop')
+
+    method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
+
+    metadata = method.call_args[1]['metadata']
+    assert len(metadata) == 1
+    assert metadata[0][0] == 'x-goog-api-client'
+    assert 'api-core' in metadata[0][1]
+
+
+def test_wrap_method_with_custom_metadata():
+    method = mock.Mock(spec=['__call__'])
+
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+        method, metadata={'foo': 'bar'})
+
+    wrapped_method(1, 2, meep='moop')
+
+    method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
+
+    metadata = method.call_args[1]['metadata']
+    assert len(metadata) == 2
+    assert ('foo', 'bar') in metadata
+
+
+def test_wrap_method_with_merged_metadata():
+    method = mock.Mock(spec=['__call__'])
+
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+        method, metadata={'x-goog-api-client': 'foo/1.2.3'})
+
+    wrapped_method(1, 2, meep='moop')
+
+    method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
+
+    metadata = method.call_args[1]['metadata']
+    assert len(metadata) == 1
+    assert metadata[0][0] == 'x-goog-api-client'
+    assert metadata[0][1].endswith(' foo/1.2.3')
+
+
+@mock.patch('time.sleep')
+def test_wrap_method_with_default_retry_and_timeout(unusued_sleep):
+    method = mock.Mock(
+        spec=['__call__'],
+        side_effect=[exceptions.InternalServerError(None), 42]
+    )
+    default_retry = retry.Retry()
+    default_timeout = timeout.ConstantTimeout(60)
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+        method, default_retry, default_timeout)
+
+    result = wrapped_method()
+
+    assert result == 42
+    assert method.call_count == 2
+    method.assert_called_with(timeout=60, metadata=mock.ANY)
+
+
+@mock.patch('time.sleep')
+def test_wrap_method_with_default_retry_and_timeout_using_sentinel(
+        unusued_sleep):
+    method = mock.Mock(
+        spec=['__call__'],
+        side_effect=[exceptions.InternalServerError(None), 42]
+    )
+    default_retry = retry.Retry()
+    default_timeout = timeout.ConstantTimeout(60)
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+        method, default_retry, default_timeout)
+
+    result = wrapped_method(
+        retry=google.api_core.gapic_v1.method.DEFAULT,
+        timeout=google.api_core.gapic_v1.method.DEFAULT)
+
+    assert result == 42
+    assert method.call_count == 2
+    method.assert_called_with(timeout=60, metadata=mock.ANY)
+
+
+@mock.patch('time.sleep')
+def test_wrap_method_with_overriding_retry_and_timeout(unusued_sleep):
+    method = mock.Mock(
+        spec=['__call__'],
+        side_effect=[exceptions.NotFound(None), 42]
+    )
+    default_retry = retry.Retry()
+    default_timeout = timeout.ConstantTimeout(60)
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+        method, default_retry, default_timeout)
+
+    result = wrapped_method(
+        retry=retry.Retry(retry.if_exception_type(exceptions.NotFound)),
+        timeout=timeout.ConstantTimeout(22))
+
+    assert result == 42
+    assert method.call_count == 2
+    method.assert_called_with(timeout=22, metadata=mock.ANY)
+
+
+@mock.patch('time.sleep')
+@mock.patch(
+    'google.api_core.datetime_helpers.utcnow',
+    side_effect=_utcnow_monotonic(),
+    autospec=True)
+def test_wrap_method_with_overriding_retry_deadline(utcnow, unused_sleep):
+    method = mock.Mock(
+        spec=['__call__'],
+        side_effect=([exceptions.InternalServerError(None)] * 4) + [42]
+    )
+    default_retry = retry.Retry()
+    default_timeout = timeout.ExponentialTimeout(deadline=60)
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+        method, default_retry, default_timeout)
+
+    # Overriding only the retry's deadline should also override the timeout's
+    # deadline.
+    result = wrapped_method(
+        retry=default_retry.with_deadline(30))
+
+    assert result == 42
+    timeout_args = [call[1]['timeout'] for call in method.call_args_list]
+    assert timeout_args == [5.0, 10.0, 20.0, 26.0, 25.0]
+    assert utcnow.call_count == (
+        1 +  # First to set the deadline.
+        5 +  # One for each min(timeout, maximum, (DEADLINE - NOW).seconds)
+        5
+    )
+
+
+def test_wrap_method_with_overriding_timeout_as_a_number():
+    method = mock.Mock(spec=['__call__'], return_value=42)
+    default_retry = retry.Retry()
+    default_timeout = timeout.ConstantTimeout(60)
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+        method, default_retry, default_timeout)
+
+    result = wrapped_method(timeout=22)
+
+    assert result == 42
+    method.assert_called_once_with(timeout=22, metadata=mock.ANY)
+
+
+def test_wrap_with_paging():
+    page_one = mock.Mock(
+        spec=['items', 'page_token', 'next_page_token'],
+        items=[1, 2],
+        next_page_token='icanhasnextpls')
+    page_two = mock.Mock(
+        spec=['items', 'page_token', 'next_page_token'],
+        items=[3, 4],
+        next_page_token=None)
+    method = mock.Mock(
+        spec=['__call__', '__name__'], side_effect=(page_one, page_two))
+    method.__name__ = 'mockmethod'
+
+    wrapped_method = google.api_core.gapic_v1.method.wrap_with_paging(
+        method, 'items', 'page_token', 'next_page_token')
+
+    request = mock.Mock(spec=['page_token'], page_token=None)
+    result = wrapped_method(request, extra='param')
+
+    # Should return an iterator and should not have actually called the
+    # method yet.
+    assert isinstance(result, google.api_core.page_iterator.Iterator)
+    method.assert_not_called()
+    assert request.page_token is None
+
+    # Draining the iterator should call the method until no more pages are
+    # returned.
+    results = list(result)
+
+    assert results == [1, 2, 3, 4]
+    assert method.call_count == 2
+    method.assert_called_with(request, extra='param')
+    assert request.page_token == 'icanhasnextpls'
diff --git a/tests/unit/operations_v1/__init__.py b/tests/unit/operations_v1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/unit/operations_v1/__init__.py
diff --git a/tests/unit/operations_v1/test_operations_client.py b/tests/unit/operations_v1/test_operations_client.py
new file mode 100644
index 0000000..60b11b7
--- /dev/null
+++ b/tests/unit/operations_v1/test_operations_client.py
@@ -0,0 +1,101 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+import mock
+
+from google.api_core import operations_v1
+from google.api_core import page_iterator
+from google.longrunning import operations_pb2
+
+
+def make_operations_stub(channel):
+    return mock.Mock(
+        spec=[
+            'GetOperation', 'DeleteOperation', 'ListOperations',
+            'CancelOperation'])
+
+
+operations_stub_patch = mock.patch(
+    'google.longrunning.operations_pb2.OperationsStub',
+    autospec=True,
+    side_effect=make_operations_stub)
+
+
+@operations_stub_patch
+def test_constructor(operations_stub):
+    stub = make_operations_stub(None)
+    operations_stub.side_effect = None
+    operations_stub.return_value = stub
+
+    client = operations_v1.OperationsClient(mock.sentinel.channel)
+
+    assert client.operations_stub == stub
+    operations_stub.assert_called_once_with(mock.sentinel.channel)
+
+
+@operations_stub_patch
+def test_get_operation(operations_stub):
+    client = operations_v1.OperationsClient(mock.sentinel.channel)
+    client.operations_stub.GetOperation.return_value = mock.sentinel.operation
+
+    response = client.get_operation('name')
+
+    request = client.operations_stub.GetOperation.call_args[0][0]
+    assert isinstance(request, operations_pb2.GetOperationRequest)
+    assert request.name == 'name'
+
+    assert response == mock.sentinel.operation
+
+
+@operations_stub_patch
+def test_list_operations(operations_stub):
+    client = operations_v1.OperationsClient(mock.sentinel.channel)
+    operations = [
+        operations_pb2.Operation(name='1'),
+        operations_pb2.Operation(name='2')]
+    list_response = operations_pb2.ListOperationsResponse(
+        operations=operations)
+    client.operations_stub.ListOperations.return_value = list_response
+
+    response = client.list_operations('name', 'filter')
+
+    assert isinstance(response, page_iterator.Iterator)
+    assert list(response) == operations
+
+    request = client.operations_stub.ListOperations.call_args[0][0]
+    assert isinstance(request, operations_pb2.ListOperationsRequest)
+    assert request.name == 'name'
+    assert request.filter == 'filter'
+
+
+@operations_stub_patch
+def test_delete_operation(operations_stub):
+    client = operations_v1.OperationsClient(mock.sentinel.channel)
+
+    client.delete_operation('name')
+
+    request = client.operations_stub.DeleteOperation.call_args[0][0]
+    assert isinstance(request, operations_pb2.DeleteOperationRequest)
+    assert request.name == 'name'
+
+
+@operations_stub_patch
+def test_cancel_operation(operations_stub):
+    client = operations_v1.OperationsClient(mock.sentinel.channel)
+
+    client.cancel_operation('name')
+
+    request = client.operations_stub.CancelOperation.call_args[0][0]
+    assert isinstance(request, operations_pb2.CancelOperationRequest)
+    assert request.name == 'name'
diff --git a/tests/unit/test_datetime_helpers.py b/tests/unit/test_datetime_helpers.py
new file mode 100644
index 0000000..24f8dbd
--- /dev/null
+++ b/tests/unit/test_datetime_helpers.py
@@ -0,0 +1,22 @@
+# Copyright 2017, Google Inc.
+#
+# 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.
+
+import datetime
+
+from google.api_core import datetime_helpers
+
+
+def test_utcnow():
+    result = datetime_helpers.utcnow()
+    assert isinstance(result, datetime.datetime)
diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py
new file mode 100644
index 0000000..df159be
--- /dev/null
+++ b/tests/unit/test_exceptions.py
@@ -0,0 +1,201 @@
+# Copyright 2014 Google Inc.
+#
+# 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.
+
+import json
+
+import grpc
+import mock
+import requests
+from six.moves import http_client
+
+from google.api_core import exceptions
+
+
+def test_create_google_cloud_error():
+    exception = exceptions.GoogleAPICallError('Testing')
+    exception.code = 600
+    assert str(exception) == '600 Testing'
+    assert exception.message == 'Testing'
+    assert exception.errors == []
+    assert exception.response is None
+
+
+def test_create_google_cloud_error_with_args():
+    error = {
+        'domain': 'global',
+        'location': 'test',
+        'locationType': 'testing',
+        'message': 'Testing',
+        'reason': 'test',
+    }
+    response = mock.sentinel.response
+    exception = exceptions.GoogleAPICallError(
+        'Testing', [error], response=response)
+    exception.code = 600
+    assert str(exception) == '600 Testing'
+    assert exception.message == 'Testing'
+    assert exception.errors == [error]
+    assert exception.response == response
+
+
+def test_from_http_status():
+    message = 'message'
+    exception = exceptions.from_http_status(http_client.NOT_FOUND, message)
+    assert exception.code == http_client.NOT_FOUND
+    assert exception.message == message
+    assert exception.errors == []
+
+
+def test_from_http_status_with_errors_and_response():
+    message = 'message'
+    errors = ['1', '2']
+    response = mock.sentinel.response
+    exception = exceptions.from_http_status(
+        http_client.NOT_FOUND, message, errors=errors, response=response)
+
+    assert isinstance(exception, exceptions.NotFound)
+    assert exception.code == http_client.NOT_FOUND
+    assert exception.message == message
+    assert exception.errors == errors
+    assert exception.response == response
+
+
+def test_from_http_status_unknown_code():
+    message = 'message'
+    status_code = 156
+    exception = exceptions.from_http_status(status_code, message)
+    assert exception.code == status_code
+    assert exception.message == message
+
+
+def make_response(content):
+    response = requests.Response()
+    response._content = content
+    response.status_code = http_client.NOT_FOUND
+    response.request = requests.Request(
+        method='POST', url='https://example.com').prepare()
+    return response
+
+
+def test_from_http_response_no_content():
+    response = make_response(None)
+
+    exception = exceptions.from_http_response(response)
+
+    assert isinstance(exception, exceptions.NotFound)
+    assert exception.code == http_client.NOT_FOUND
+    assert exception.message == 'POST https://example.com/: unknown error'
+    assert exception.response == response
+
+
+def test_from_http_response_text_content():
+    response = make_response(b'message')
+
+    exception = exceptions.from_http_response(response)
+
+    assert isinstance(exception, exceptions.NotFound)
+    assert exception.code == http_client.NOT_FOUND
+    assert exception.message == 'POST https://example.com/: message'
+
+
+def test_from_http_response_json_content():
+    response = make_response(json.dumps({
+        'error': {
+            'message': 'json message',
+            'errors': ['1', '2']
+        }
+    }).encode('utf-8'))
+
+    exception = exceptions.from_http_response(response)
+
+    assert isinstance(exception, exceptions.NotFound)
+    assert exception.code == http_client.NOT_FOUND
+    assert exception.message == 'POST https://example.com/: json message'
+    assert exception.errors == ['1', '2']
+
+
+def test_from_http_response_bad_json_content():
+    response = make_response(json.dumps({'meep': 'moop'}).encode('utf-8'))
+
+    exception = exceptions.from_http_response(response)
+
+    assert isinstance(exception, exceptions.NotFound)
+    assert exception.code == http_client.NOT_FOUND
+    assert exception.message == 'POST https://example.com/: unknown error'
+
+
+def test_from_grpc_status():
+    message = 'message'
+    exception = exceptions.from_grpc_status(
+        grpc.StatusCode.OUT_OF_RANGE, message)
+    assert isinstance(exception, exceptions.BadRequest)
+    assert isinstance(exception, exceptions.OutOfRange)
+    assert exception.code == http_client.BAD_REQUEST
+    assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE
+    assert exception.message == message
+    assert exception.errors == []
+
+
+def test_from_grpc_status_with_errors_and_response():
+    message = 'message'
+    response = mock.sentinel.response
+    errors = ['1', '2']
+    exception = exceptions.from_grpc_status(
+        grpc.StatusCode.OUT_OF_RANGE, message,
+        errors=errors, response=response)
+
+    assert isinstance(exception, exceptions.OutOfRange)
+    assert exception.message == message
+    assert exception.errors == errors
+    assert exception.response == response
+
+
+def test_from_grpc_status_unknown_code():
+    message = 'message'
+    exception = exceptions.from_grpc_status(
+        grpc.StatusCode.OK, message)
+    assert exception.grpc_status_code == grpc.StatusCode.OK
+    assert exception.message == message
+
+
+def test_from_grpc_error():
+    message = 'message'
+    error = mock.create_autospec(grpc.Call, instance=True)
+    error.code.return_value = grpc.StatusCode.INVALID_ARGUMENT
+    error.details.return_value = message
+
+    exception = exceptions.from_grpc_error(error)
+
+    assert isinstance(exception, exceptions.BadRequest)
+    assert isinstance(exception, exceptions.InvalidArgument)
+    assert exception.code == http_client.BAD_REQUEST
+    assert exception.grpc_status_code == grpc.StatusCode.INVALID_ARGUMENT
+    assert exception.message == message
+    assert exception.errors == [error]
+    assert exception.response == error
+
+
+def test_from_grpc_error_non_call():
+    message = 'message'
+    error = mock.create_autospec(grpc.RpcError, instance=True)
+    error.__str__.return_value = message
+
+    exception = exceptions.from_grpc_error(error)
+
+    assert isinstance(exception, exceptions.GoogleAPICallError)
+    assert exception.code is None
+    assert exception.grpc_status_code is None
+    assert exception.message == message
+    assert exception.errors == [error]
+    assert exception.response == error
diff --git a/tests/unit/test_general_helpers.py b/tests/unit/test_general_helpers.py
new file mode 100644
index 0000000..b878cc5
--- /dev/null
+++ b/tests/unit/test_general_helpers.py
@@ -0,0 +1,43 @@
+# Copyright 2017, Google Inc.
+#
+# 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.
+
+import functools
+
+from google.api_core import general_helpers
+
+
+def test_wraps_normal_func():
+
+    def func():
+        return 42
+
+    @general_helpers.wraps(func)
+    def replacement():
+        return func()
+
+    assert replacement() == 42
+
+
+def test_wraps_partial():
+
+    def func():
+        return 42
+
+    partial = functools.partial(func)
+
+    @general_helpers.wraps(partial)
+    def replacement():
+        return func()
+
+    assert replacement() == 42
diff --git a/tests/unit/test_grpc_helpers.py b/tests/unit/test_grpc_helpers.py
new file mode 100644
index 0000000..d5e0b3c
--- /dev/null
+++ b/tests/unit/test_grpc_helpers.py
@@ -0,0 +1,171 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+import grpc
+import mock
+import pytest
+
+from google.api_core import exceptions
+from google.api_core import grpc_helpers
+
+
+def test__patch_callable_name():
+    callable = mock.Mock(spec=['__class__'])
+    callable.__class__ = mock.Mock(spec=['__name__'])
+    callable.__class__.__name__ = 'TestCallable'
+
+    grpc_helpers._patch_callable_name(callable)
+
+    assert callable.__name__ == 'TestCallable'
+
+
+def test__patch_callable_name_no_op():
+    callable = mock.Mock(spec=['__name__'])
+    callable.__name__ = 'test_callable'
+
+    grpc_helpers._patch_callable_name(callable)
+
+    assert callable.__name__ == 'test_callable'
+
+
+class RpcErrorImpl(grpc.RpcError, grpc.Call):
+    def __init__(self, code):
+        super(RpcErrorImpl, self).__init__()
+        self._code = code
+
+    def code(self):
+        return self._code
+
+    def details(self):
+        return None
+
+
+def test_wrap_unary_errors():
+    grpc_error = RpcErrorImpl(grpc.StatusCode.INVALID_ARGUMENT)
+    callable_ = mock.Mock(spec=['__call__'], side_effect=grpc_error)
+
+    wrapped_callable = grpc_helpers._wrap_unary_errors(callable_)
+
+    with pytest.raises(exceptions.InvalidArgument) as exc_info:
+        wrapped_callable(1, 2, three='four')
+
+    callable_.assert_called_once_with(1, 2, three='four')
+    assert exc_info.value.response == grpc_error
+
+
+def test_wrap_stream_errors_invocation():
+    grpc_error = RpcErrorImpl(grpc.StatusCode.INVALID_ARGUMENT)
+    callable_ = mock.Mock(spec=['__call__'], side_effect=grpc_error)
+
+    wrapped_callable = grpc_helpers._wrap_stream_errors(callable_)
+
+    with pytest.raises(exceptions.InvalidArgument) as exc_info:
+        wrapped_callable(1, 2, three='four')
+
+    callable_.assert_called_once_with(1, 2, three='four')
+    assert exc_info.value.response == grpc_error
+
+
+class RpcResponseIteratorImpl(object):
+    def __init__(self, exception):
+        self._exception = exception
+
+    # Note: This matches grpc._channel._Rendezvous._next which is what is
+    # patched by _wrap_stream_errors.
+    def _next(self):
+        raise self._exception
+
+    def __next__(self):  # pragma: NO COVER
+        return self._next()
+
+    def next(self):  # pragma: NO COVER
+        return self._next()
+
+
+def test_wrap_stream_errors_iterator():
+    grpc_error = RpcErrorImpl(grpc.StatusCode.UNAVAILABLE)
+    response_iter = RpcResponseIteratorImpl(grpc_error)
+    callable_ = mock.Mock(spec=['__call__'], return_value=response_iter)
+
+    wrapped_callable = grpc_helpers._wrap_stream_errors(callable_)
+
+    got_iterator = wrapped_callable(1, 2, three='four')
+
+    with pytest.raises(exceptions.ServiceUnavailable) as exc_info:
+        next(got_iterator)
+
+    assert got_iterator == response_iter
+    callable_.assert_called_once_with(1, 2, three='four')
+    assert exc_info.value.response == grpc_error
+
+
+@mock.patch('google.api_core.grpc_helpers._wrap_unary_errors')
+def test_wrap_errors_non_streaming(wrap_unary_errors):
+    callable_ = mock.create_autospec(grpc.UnaryUnaryMultiCallable)
+
+    result = grpc_helpers.wrap_errors(callable_)
+
+    assert result == wrap_unary_errors.return_value
+    wrap_unary_errors.assert_called_once_with(callable_)
+
+
+@mock.patch('google.api_core.grpc_helpers._wrap_stream_errors')
+def test_wrap_errors_streaming(wrap_stream_errors):
+    callable_ = mock.create_autospec(grpc.UnaryStreamMultiCallable)
+
+    result = grpc_helpers.wrap_errors(callable_)
+
+    assert result == wrap_stream_errors.return_value
+    wrap_stream_errors.assert_called_once_with(callable_)
+
+
+@mock.patch(
+    'google.auth.default',
+    return_value=(mock.sentinel.credentials, mock.sentinel.projet))
+@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
+def test_create_channel_implicit(secure_authorized_channel, default):
+    target = 'example.com:443'
+
+    channel = grpc_helpers.create_channel(target)
+
+    assert channel is secure_authorized_channel.return_value
+    default.assert_called_once_with(scopes=None)
+    secure_authorized_channel.assert_called_once_with(
+        mock.sentinel.credentials, mock.ANY, target)
+
+
+@mock.patch(
+    'google.auth.default',
+    return_value=(mock.sentinel.credentials, mock.sentinel.projet))
+@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
+def test_create_channel_implicit_with_scopes(
+        secure_authorized_channel, default):
+    target = 'example.com:443'
+
+    channel = grpc_helpers.create_channel(target, scopes=['one', 'two'])
+
+    assert channel is secure_authorized_channel.return_value
+    default.assert_called_once_with(scopes=['one', 'two'])
+
+
+@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
+def test_create_channel_explicit(secure_authorized_channel):
+    target = 'example.com:443'
+
+    channel = grpc_helpers.create_channel(
+        target, credentials=mock.sentinel.credentials)
+
+    assert channel is secure_authorized_channel.return_value
+    secure_authorized_channel.assert_called_once_with(
+        mock.sentinel.credentials, mock.ANY, target)
diff --git a/tests/unit/test_operation.py b/tests/unit/test_operation.py
new file mode 100644
index 0000000..1d765cc
--- /dev/null
+++ b/tests/unit/test_operation.py
@@ -0,0 +1,223 @@
+# Copyright 2017, Google Inc.
+#
+# 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.
+
+
+import mock
+
+from google.api_core import operation
+from google.api_core import operations_v1
+from google.longrunning import operations_pb2
+from google.protobuf import struct_pb2
+from google.rpc import code_pb2
+from google.rpc import status_pb2
+
+TEST_OPERATION_NAME = 'test/operation'
+
+
+def make_operation_proto(
+        name=TEST_OPERATION_NAME, metadata=None, response=None,
+        error=None, **kwargs):
+    operation_proto = operations_pb2.Operation(
+        name=name, **kwargs)
+
+    if metadata is not None:
+        operation_proto.metadata.Pack(metadata)
+
+    if response is not None:
+        operation_proto.response.Pack(response)
+
+    if error is not None:
+        operation_proto.error.CopyFrom(error)
+
+    return operation_proto
+
+
+def make_operation_future(client_operations_responses=None):
+    if client_operations_responses is None:
+        client_operations_responses = [make_operation_proto()]
+
+    refresh = mock.Mock(
+        spec=['__call__'], side_effect=client_operations_responses)
+    refresh.responses = client_operations_responses
+    cancel = mock.Mock(spec=['__call__'])
+    operation_future = operation.Operation(
+        client_operations_responses[0],
+        refresh,
+        cancel,
+        result_type=struct_pb2.Struct,
+        metadata_type=struct_pb2.Struct)
+
+    return operation_future, refresh, cancel
+
+
+def test_constructor():
+    future, refresh, _ = make_operation_future()
+
+    assert future.operation == refresh.responses[0]
+    assert future.operation.done is False
+    assert future.operation.name == TEST_OPERATION_NAME
+    assert future.metadata is None
+    assert future.running()
+
+
+def test_metadata():
+    expected_metadata = struct_pb2.Struct()
+    future, _, _ = make_operation_future(
+        [make_operation_proto(metadata=expected_metadata)])
+
+    assert future.metadata == expected_metadata
+
+
+def test_cancellation():
+    responses = [
+        make_operation_proto(),
+        # Second response indicates that the operation was cancelled.
+        make_operation_proto(
+            done=True,
+            error=status_pb2.Status(code=code_pb2.CANCELLED))]
+    future, _, cancel = make_operation_future(responses)
+
+    assert future.cancel()
+    assert future.cancelled()
+    cancel.assert_called_once_with()
+
+    # Cancelling twice should have no effect.
+    assert not future.cancel()
+    cancel.assert_called_once_with()
+
+
+def test_result():
+    expected_result = struct_pb2.Struct()
+    responses = [
+        make_operation_proto(),
+        # Second operation response includes the result.
+        make_operation_proto(done=True, response=expected_result)]
+    future, _, _ = make_operation_future(responses)
+
+    result = future.result()
+
+    assert result == expected_result
+    assert future.done()
+
+
+def test_exception():
+    expected_exception = status_pb2.Status(message='meep')
+    responses = [
+        make_operation_proto(),
+        # Second operation response includes the error.
+        make_operation_proto(done=True, error=expected_exception)]
+    future, _, _ = make_operation_future(responses)
+
+    exception = future.exception()
+
+    assert expected_exception.message in '{!r}'.format(exception)
+
+
+def test_unexpected_result():
+    responses = [
+        make_operation_proto(),
+        # Second operation response is done, but has not error or response.
+        make_operation_proto(done=True)]
+    future, _, _ = make_operation_future(responses)
+
+    exception = future.exception()
+
+    assert 'Unexpected state' in '{!r}'.format(exception)
+
+
+def test__refresh_http():
+    api_request = mock.Mock(
+        return_value={'name': TEST_OPERATION_NAME, 'done': True})
+
+    result = operation._refresh_http(api_request, TEST_OPERATION_NAME)
+
+    assert result.name == TEST_OPERATION_NAME
+    assert result.done is True
+    api_request.assert_called_once_with(
+        method='GET', path='operations/{}'.format(TEST_OPERATION_NAME))
+
+
+def test__cancel_http():
+    api_request = mock.Mock()
+
+    operation._cancel_http(api_request, TEST_OPERATION_NAME)
+
+    api_request.assert_called_once_with(
+        method='POST', path='operations/{}:cancel'.format(TEST_OPERATION_NAME))
+
+
+def test_from_http_json():
+    operation_json = {'name': TEST_OPERATION_NAME, 'done': True}
+    api_request = mock.sentinel.api_request
+
+    future = operation.from_http_json(
+        operation_json, api_request, struct_pb2.Struct,
+        metadata_type=struct_pb2.Struct)
+
+    assert future._result_type == struct_pb2.Struct
+    assert future._metadata_type == struct_pb2.Struct
+    assert future.operation.name == TEST_OPERATION_NAME
+    assert future.done
+
+
+def test__refresh_grpc():
+    operations_stub = mock.Mock(spec=['GetOperation'])
+    expected_result = make_operation_proto(done=True)
+    operations_stub.GetOperation.return_value = expected_result
+
+    result = operation._refresh_grpc(operations_stub, TEST_OPERATION_NAME)
+
+    assert result == expected_result
+    expected_request = operations_pb2.GetOperationRequest(
+        name=TEST_OPERATION_NAME)
+    operations_stub.GetOperation.assert_called_once_with(expected_request)
+
+
+def test__cancel_grpc():
+    operations_stub = mock.Mock(spec=['CancelOperation'])
+
+    operation._cancel_grpc(operations_stub, TEST_OPERATION_NAME)
+
+    expected_request = operations_pb2.CancelOperationRequest(
+        name=TEST_OPERATION_NAME)
+    operations_stub.CancelOperation.assert_called_once_with(expected_request)
+
+
+def test_from_grpc():
+    operation_proto = make_operation_proto(done=True)
+    operations_stub = mock.sentinel.operations_stub
+
+    future = operation.from_grpc(
+        operation_proto, operations_stub, struct_pb2.Struct,
+        metadata_type=struct_pb2.Struct)
+
+    assert future._result_type == struct_pb2.Struct
+    assert future._metadata_type == struct_pb2.Struct
+    assert future.operation.name == TEST_OPERATION_NAME
+    assert future.done
+
+
+def test_from_gapic():
+    operation_proto = make_operation_proto(done=True)
+    operations_client = mock.create_autospec(
+        operations_v1.OperationsClient, instance=True)
+
+    future = operation.from_gapic(
+        operation_proto, operations_client, struct_pb2.Struct,
+        metadata_type=struct_pb2.Struct)
+
+    assert future._result_type == struct_pb2.Struct
+    assert future._metadata_type == struct_pb2.Struct
+    assert future.operation.name == TEST_OPERATION_NAME
+    assert future.done
diff --git a/tests/unit/test_page_iterator.py b/tests/unit/test_page_iterator.py
new file mode 100644
index 0000000..5cecac8
--- /dev/null
+++ b/tests/unit/test_page_iterator.py
@@ -0,0 +1,545 @@
+# Copyright 2015 Google Inc.
+#
+# 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.
+
+import types
+
+import mock
+import pytest
+import six
+
+from google.api_core import page_iterator
+
+
+def test__do_nothing_page_start():
+    assert page_iterator._do_nothing_page_start(None, None, None) is None
+
+
+class TestPage(object):
+
+    def test_constructor(self):
+        parent = mock.sentinel.parent
+        item_to_value = mock.sentinel.item_to_value
+
+        page = page_iterator.Page(parent, (1, 2, 3), item_to_value)
+
+        assert page.num_items == 3
+        assert page.remaining == 3
+        assert page._parent is parent
+        assert page._item_to_value is item_to_value
+
+    def test___iter__(self):
+        page = page_iterator.Page(None, (), None)
+        assert iter(page) is page
+
+    def test_iterator_calls_parent_item_to_value(self):
+        parent = mock.sentinel.parent
+
+        item_to_value = mock.Mock(
+            side_effect=lambda iterator, value: value, spec=['__call__'])
+
+        page = page_iterator.Page(parent, (10, 11, 12), item_to_value)
+        page._remaining = 100
+
+        assert item_to_value.call_count == 0
+        assert page.remaining == 100
+
+        assert six.next(page) == 10
+        assert item_to_value.call_count == 1
+        item_to_value.assert_called_with(parent, 10)
+        assert page.remaining == 99
+
+        assert six.next(page) == 11
+        assert item_to_value.call_count == 2
+        item_to_value.assert_called_with(parent, 11)
+        assert page.remaining == 98
+
+        assert six.next(page) == 12
+        assert item_to_value.call_count == 3
+        item_to_value.assert_called_with(parent, 12)
+        assert page.remaining == 97
+
+
+class PageIteratorImpl(page_iterator.Iterator):
+    def _next_page(self):
+        return mock.create_autospec(page_iterator.Page, instance=True)
+
+
+class TestIterator(object):
+
+    def test_constructor(self):
+        client = mock.sentinel.client
+        item_to_value = mock.sentinel.item_to_value
+        token = 'ab13nceor03'
+        max_results = 1337
+
+        iterator = PageIteratorImpl(
+            client, item_to_value, page_token=token, max_results=max_results)
+
+        assert not iterator._started
+        assert iterator.client is client
+        assert iterator._item_to_value == item_to_value
+        assert iterator.max_results == max_results
+        # Changing attributes.
+        assert iterator.page_number == 0
+        assert iterator.next_page_token == token
+        assert iterator.num_results == 0
+
+    def test_pages_property_starts(self):
+        iterator = PageIteratorImpl(None, None)
+
+        assert not iterator._started
+
+        assert isinstance(iterator.pages, types.GeneratorType)
+
+        assert iterator._started
+
+    def test_pages_property_restart(self):
+        iterator = PageIteratorImpl(None, None)
+
+        assert iterator.pages
+
+        # Make sure we cannot restart.
+        with pytest.raises(ValueError):
+            assert iterator.pages
+
+    def test__page_iter_increment(self):
+        iterator = PageIteratorImpl(None, None)
+        page = page_iterator.Page(
+            iterator, ('item',), page_iterator._item_to_value_identity)
+        iterator._next_page = mock.Mock(side_effect=[page, None])
+
+        assert iterator.num_results == 0
+
+        page_iter = iterator._page_iter(increment=True)
+        next(page_iter)
+
+        assert iterator.num_results == 1
+
+    def test__page_iter_no_increment(self):
+        iterator = PageIteratorImpl(None, None)
+
+        assert iterator.num_results == 0
+
+        page_iter = iterator._page_iter(increment=False)
+        next(page_iter)
+
+        # results should still be 0 after fetching a page.
+        assert iterator.num_results == 0
+
+    def test__items_iter(self):
+        # Items to be returned.
+        item1 = 17
+        item2 = 100
+        item3 = 211
+
+        # Make pages from mock responses
+        parent = mock.sentinel.parent
+        page1 = page_iterator.Page(
+            parent, (item1, item2), page_iterator._item_to_value_identity)
+        page2 = page_iterator.Page(
+            parent, (item3,), page_iterator._item_to_value_identity)
+
+        iterator = PageIteratorImpl(None, None)
+        iterator._next_page = mock.Mock(side_effect=[page1, page2, None])
+
+        items_iter = iterator._items_iter()
+
+        assert isinstance(items_iter, types.GeneratorType)
+
+        # Consume items and check the state of the iterator.
+        assert iterator.num_results == 0
+
+        assert six.next(items_iter) == item1
+        assert iterator.num_results == 1
+
+        assert six.next(items_iter) == item2
+        assert iterator.num_results == 2
+
+        assert six.next(items_iter) == item3
+        assert iterator.num_results == 3
+
+        with pytest.raises(StopIteration):
+            six.next(items_iter)
+
+    def test___iter__(self):
+        iterator = PageIteratorImpl(None, None)
+        iterator._next_page = mock.Mock(side_effect=[(1, 2), (3,), None])
+
+        assert not iterator._started
+
+        result = list(iterator)
+
+        assert result == [1, 2, 3]
+        assert iterator._started
+
+    def test___iter__restart(self):
+        iterator = PageIteratorImpl(None, None)
+
+        iter(iterator)
+
+        # Make sure we cannot restart.
+        with pytest.raises(ValueError):
+            iter(iterator)
+
+    def test___iter___restart_after_page(self):
+        iterator = PageIteratorImpl(None, None)
+
+        assert iterator.pages
+
+        # Make sure we cannot restart after starting the page iterator
+        with pytest.raises(ValueError):
+            iter(iterator)
+
+
+class TestHTTPIterator(object):
+
+    def test_constructor(self):
+        client = mock.sentinel.client
+        path = '/foo'
+        iterator = page_iterator.HTTPIterator(
+            client, mock.sentinel.api_request,
+            path, mock.sentinel.item_to_value)
+
+        assert not iterator._started
+        assert iterator.client is client
+        assert iterator.path == path
+        assert iterator._item_to_value is mock.sentinel.item_to_value
+        assert iterator._items_key == 'items'
+        assert iterator.max_results is None
+        assert iterator.extra_params == {}
+        assert iterator._page_start == page_iterator._do_nothing_page_start
+        # Changing attributes.
+        assert iterator.page_number == 0
+        assert iterator.next_page_token is None
+        assert iterator.num_results == 0
+
+    def test_constructor_w_extra_param_collision(self):
+        extra_params = {'pageToken': 'val'}
+
+        with pytest.raises(ValueError):
+            page_iterator.HTTPIterator(
+                mock.sentinel.client,
+                mock.sentinel.api_request,
+                mock.sentinel.path,
+                mock.sentinel.item_to_value,
+                extra_params=extra_params)
+
+    def test_iterate(self):
+        path = '/foo'
+        item1 = {'name': '1'}
+        item2 = {'name': '2'}
+        api_request = mock.Mock(return_value={'items': [item1, item2]})
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client, api_request, path=path,
+            item_to_value=page_iterator._item_to_value_identity)
+
+        assert iterator.num_results == 0
+
+        items_iter = iter(iterator)
+
+        val1 = six.next(items_iter)
+        assert val1 == item1
+        assert iterator.num_results == 1
+
+        val2 = six.next(items_iter)
+        assert val2 == item2
+        assert iterator.num_results == 2
+
+        with pytest.raises(StopIteration):
+            six.next(items_iter)
+
+        api_request.assert_called_once_with(
+            method='GET', path=path, query_params={})
+
+    def test__has_next_page_new(self):
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value)
+
+        # The iterator should *always* indicate that it has a next page
+        # when created so that it can fetch the initial page.
+        assert iterator._has_next_page()
+
+    def test__has_next_page_without_token(self):
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value)
+
+        iterator.page_number = 1
+
+        # The iterator should not indicate that it has a new page if the
+        # initial page has been requested and there's no page token.
+        assert not iterator._has_next_page()
+
+    def test__has_next_page_w_number_w_token(self):
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value)
+
+        iterator.page_number = 1
+        iterator.next_page_token = mock.sentinel.token
+
+        # The iterator should indicate that it has a new page if the
+        # initial page has been requested and there's is a page token.
+        assert iterator._has_next_page()
+
+    def test__has_next_page_w_max_results_not_done(self):
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value,
+            max_results=3,
+            page_token=mock.sentinel.token)
+
+        iterator.page_number = 1
+
+        # The iterator should indicate that it has a new page if there
+        # is a page token and it has not consumed more than max_results.
+        assert iterator.num_results < iterator.max_results
+        assert iterator._has_next_page()
+
+    def test__has_next_page_w_max_results_done(self):
+
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value,
+            max_results=3,
+            page_token=mock.sentinel.token)
+
+        iterator.page_number = 1
+        iterator.num_results = 3
+
+        # The iterator should not indicate that it has a new page if there
+        # if it has consumed more than max_results.
+        assert iterator.num_results == iterator.max_results
+        assert not iterator._has_next_page()
+
+    def test__get_query_params_no_token(self):
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value)
+
+        assert iterator._get_query_params() == {}
+
+    def test__get_query_params_w_token(self):
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value)
+        iterator.next_page_token = 'token'
+
+        assert iterator._get_query_params() == {
+            'pageToken': iterator.next_page_token}
+
+    def test__get_query_params_w_max_results(self):
+        max_results = 3
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value,
+            max_results=max_results)
+
+        iterator.num_results = 1
+        local_max = max_results - iterator.num_results
+
+        assert iterator._get_query_params() == {
+            'maxResults': local_max}
+
+    def test__get_query_params_extra_params(self):
+        extra_params = {'key': 'val'}
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value,
+            extra_params=extra_params)
+
+        assert iterator._get_query_params() == extra_params
+
+    def test__get_next_page_response_with_post(self):
+        path = '/foo'
+        page_response = {'items': ['one', 'two']}
+        api_request = mock.Mock(return_value=page_response)
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client, api_request, path=path,
+            item_to_value=page_iterator._item_to_value_identity)
+        iterator._HTTP_METHOD = 'POST'
+
+        response = iterator._get_next_page_response()
+
+        assert response == page_response
+
+        api_request.assert_called_once_with(
+            method='POST', path=path, data={})
+
+    def test__get_next_page_bad_http_method(self):
+        iterator = page_iterator.HTTPIterator(
+            mock.sentinel.client,
+            mock.sentinel.api_request,
+            mock.sentinel.path,
+            mock.sentinel.item_to_value)
+        iterator._HTTP_METHOD = 'NOT-A-VERB'
+
+        with pytest.raises(ValueError):
+            iterator._get_next_page_response()
+
+
+class TestGRPCIterator(object):
+
+    def test_constructor(self):
+        client = mock.sentinel.client
+        items_field = 'items'
+        iterator = page_iterator.GRPCIterator(
+            client, mock.sentinel.method, mock.sentinel.request, items_field)
+
+        assert not iterator._started
+        assert iterator.client is client
+        assert iterator.max_results is None
+        assert iterator._method == mock.sentinel.method
+        assert iterator._request == mock.sentinel.request
+        assert iterator._items_field == items_field
+        assert iterator._item_to_value is page_iterator._item_to_value_identity
+        assert (iterator._request_token_field ==
+                page_iterator.GRPCIterator._DEFAULT_REQUEST_TOKEN_FIELD)
+        assert (iterator._response_token_field ==
+                page_iterator.GRPCIterator._DEFAULT_RESPONSE_TOKEN_FIELD)
+        # Changing attributes.
+        assert iterator.page_number == 0
+        assert iterator.next_page_token is None
+        assert iterator.num_results == 0
+
+    def test_constructor_options(self):
+        client = mock.sentinel.client
+        items_field = 'items'
+        request_field = 'request'
+        response_field = 'response'
+        iterator = page_iterator.GRPCIterator(
+            client, mock.sentinel.method, mock.sentinel.request, items_field,
+            item_to_value=mock.sentinel.item_to_value,
+            request_token_field=request_field,
+            response_token_field=response_field,
+            max_results=42)
+
+        assert iterator.client is client
+        assert iterator.max_results == 42
+        assert iterator._method == mock.sentinel.method
+        assert iterator._request == mock.sentinel.request
+        assert iterator._items_field == items_field
+        assert iterator._item_to_value is mock.sentinel.item_to_value
+        assert iterator._request_token_field == request_field
+        assert iterator._response_token_field == response_field
+
+    def test_iterate(self):
+        request = mock.Mock(spec=['page_token'], page_token=None)
+        response1 = mock.Mock(items=['a', 'b'], next_page_token='1')
+        response2 = mock.Mock(items=['c'], next_page_token='2')
+        response3 = mock.Mock(items=['d'], next_page_token='')
+        method = mock.Mock(side_effect=[response1, response2, response3])
+        iterator = page_iterator.GRPCIterator(
+            mock.sentinel.client, method, request, 'items')
+
+        assert iterator.num_results == 0
+
+        items = list(iterator)
+        assert items == ['a', 'b', 'c', 'd']
+
+        method.assert_called_with(request)
+        assert method.call_count == 3
+        assert request.page_token == '2'
+
+    def test_iterate_with_max_results(self):
+        request = mock.Mock(spec=['page_token'], page_token=None)
+        response1 = mock.Mock(items=['a', 'b'], next_page_token='1')
+        response2 = mock.Mock(items=['c'], next_page_token='2')
+        response3 = mock.Mock(items=['d'], next_page_token='')
+        method = mock.Mock(side_effect=[response1, response2, response3])
+        iterator = page_iterator.GRPCIterator(
+            mock.sentinel.client, method, request, 'items', max_results=3)
+
+        assert iterator.num_results == 0
+
+        items = list(iterator)
+
+        assert items == ['a', 'b', 'c']
+        assert iterator.num_results == 3
+
+        method.assert_called_with(request)
+        assert method.call_count == 2
+        assert request.page_token is '1'
+
+
+class GAXPageIterator(object):
+    """Fake object that matches gax.PageIterator"""
+    def __init__(self, pages, page_token=None):
+        self._pages = iter(pages)
+        self.page_token = page_token
+
+    def next(self):
+        return six.next(self._pages)
+
+    __next__ = next
+
+
+class TestGAXIterator(object):
+
+    def test_constructor(self):
+        client = mock.sentinel.client
+        token = 'zzzyy78kl'
+        page_iter = GAXPageIterator((), page_token=token)
+        item_to_value = page_iterator._item_to_value_identity
+        max_results = 1337
+        iterator = page_iterator._GAXIterator(
+            client, page_iter, item_to_value, max_results=max_results)
+
+        assert not iterator._started
+        assert iterator.client is client
+        assert iterator._item_to_value is item_to_value
+        assert iterator.max_results == max_results
+        assert iterator._gax_page_iter is page_iter
+        # Changing attributes.
+        assert iterator.page_number == 0
+        assert iterator.next_page_token == token
+        assert iterator.num_results == 0
+
+    def test__next_page(self):
+        page_items = (29, 31)
+        page_token = '2sde98ds2s0hh'
+        page_iter = GAXPageIterator([page_items], page_token=page_token)
+        iterator = page_iterator._GAXIterator(
+            mock.sentinel.client,
+            page_iter,
+            page_iterator._item_to_value_identity)
+
+        page = iterator._next_page()
+
+        assert iterator.next_page_token == page_token
+        assert isinstance(page, page_iterator.Page)
+        assert list(page) == list(page_items)
+
+        next_page = iterator._next_page()
+
+        assert next_page is None
diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py
new file mode 100644
index 0000000..daeeeec
--- /dev/null
+++ b/tests/unit/test_path_template.py
@@ -0,0 +1,90 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+from __future__ import unicode_literals
+
+import mock
+import pytest
+
+from google.api_core import path_template
+
+
+@pytest.mark.parametrize('tmpl, args, kwargs, expected_result', [
+    # Basic positional params
+    ['/v1/*', ['a'], {}, '/v1/a'],
+    ['/v1/**', ['a/b'], {}, '/v1/a/b'],
+    ['/v1/*/*', ['a', 'b'], {}, '/v1/a/b'],
+    ['/v1/*/*/**', ['a', 'b', 'c/d'], {}, '/v1/a/b/c/d'],
+    # Basic named params
+    ['/v1/{name}', [], {'name': 'parent'}, '/v1/parent'],
+    ['/v1/{name=**}', [], {'name': 'parent/child'}, '/v1/parent/child'],
+    # Named params with a sub-template
+    ['/v1/{name=parent/*}', [], {'name': 'parent/child'}, '/v1/parent/child'],
+    ['/v1/{name=parent/**}', [], {'name': 'parent/child/object'},
+     '/v1/parent/child/object'],
+    # Combining positional and named params
+    ['/v1/*/{name}', ['a'], {'name': 'parent'}, '/v1/a/parent'],
+    ['/v1/{name}/*', ['a'], {'name': 'parent'}, '/v1/parent/a'],
+    ['/v1/{parent}/*/{child}/*', ['a', 'b'],
+     {'parent': 'thor', 'child': 'thorson'}, '/v1/thor/a/thorson/b'],
+    ['/v1/{name}/**', ['a/b'], {'name': 'parent'}, '/v1/parent/a/b'],
+    # Combining positional and named params with sub-templates.
+    ['/v1/{name=parent/*}/*', ['a'], {'name': 'parent/child'},
+     '/v1/parent/child/a'],
+    ['/v1/*/{name=parent/**}', ['a'], {'name': 'parent/child/object'},
+     '/v1/a/parent/child/object'],
+])
+def test_expand_success(tmpl, args, kwargs, expected_result):
+    result = path_template.expand(tmpl, *args, **kwargs)
+    assert result == expected_result
+    assert path_template.validate(tmpl, result)
+
+
+@pytest.mark.parametrize('tmpl, args, kwargs, exc_match', [
+    # Missing positional arg.
+    ['v1/*', [], {}, 'Positional'],
+    # Missing named arg.
+    ['v1/{name}', [], {}, 'Named'],
+])
+def test_expanded_failure(tmpl, args, kwargs, exc_match):
+    with pytest.raises(ValueError, match=exc_match):
+        path_template.expand(tmpl, *args, **kwargs)
+
+
+@pytest.mark.parametrize('tmpl, path', [
+    # Single segment template, but multi segment value
+    ['v1/*', 'v1/a/b'],
+    ['v1/*/*', 'v1/a/b/c'],
+    # Single segement named template, but multi segment value
+    ['v1/{name}', 'v1/a/b'],
+    ['v1/{name}/{value}', 'v1/a/b/c'],
+    # Named value with a sub-template but invalid value
+    ['v1/{name=parent/*}', 'v1/grandparent/child'],
+])
+def test_validate_failure(tmpl, path):
+    assert not path_template.validate(tmpl, path)
+
+
+def test__expand_variable_match_unexpected():
+    match = mock.Mock(spec=['group'])
+    match.group.return_value = None
+    with pytest.raises(ValueError, match='Unknown'):
+        path_template._expand_variable_match([], {}, match)
+
+
+def test__replace_variable_with_pattern():
+    match = mock.Mock(spec=['group'])
+    match.group.return_value = None
+    with pytest.raises(ValueError, match='Unknown'):
+        path_template._replace_variable_with_pattern(match)
diff --git a/tests/unit/test_protobuf_helpers.py b/tests/unit/test_protobuf_helpers.py
new file mode 100644
index 0000000..6233536
--- /dev/null
+++ b/tests/unit/test_protobuf_helpers.py
@@ -0,0 +1,37 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+import pytest
+
+from google.api_core import protobuf_helpers
+from google.protobuf import any_pb2
+from google.type import date_pb2
+from google.type import timeofday_pb2
+
+
+def test_from_any_pb_success():
+    in_message = date_pb2.Date(year=1990)
+    in_message_any = any_pb2.Any()
+    in_message_any.Pack(in_message)
+    out_message = protobuf_helpers.from_any_pb(date_pb2.Date, in_message_any)
+
+    assert in_message == out_message
+
+
+def test_from_any_pb_failure():
+    in_message = any_pb2.Any()
+    in_message.Pack(date_pb2.Date(year=1990))
+
+    with pytest.raises(TypeError):
+        protobuf_helpers.from_any_pb(timeofday_pb2.TimeOfDay, in_message)
diff --git a/tests/unit/test_retry.py b/tests/unit/test_retry.py
new file mode 100644
index 0000000..a671ad3
--- /dev/null
+++ b/tests/unit/test_retry.py
@@ -0,0 +1,255 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+import datetime
+import itertools
+import re
+
+import mock
+import pytest
+
+from google.api_core import exceptions
+from google.api_core import retry
+
+
+def test_if_exception_type():
+    predicate = retry.if_exception_type(ValueError)
+
+    assert predicate(ValueError())
+    assert not predicate(TypeError())
+
+
+def test_if_exception_type_multiple():
+    predicate = retry.if_exception_type(ValueError, TypeError)
+
+    assert predicate(ValueError())
+    assert predicate(TypeError())
+    assert not predicate(RuntimeError())
+
+
+def test_if_transient_error():
+    assert retry.if_transient_error(exceptions.InternalServerError(''))
+    assert retry.if_transient_error(exceptions.TooManyRequests(''))
+    assert not retry.if_transient_error(exceptions.InvalidArgument(''))
+
+
+# Make uniform return half of its maximum, which will be the calculated
+# sleep time.
+@mock.patch('random.uniform', autospec=True, side_effect=lambda m, n: n/2.0)
+def test_exponential_sleep_generator_base_2(uniform):
+    gen = retry.exponential_sleep_generator(
+        1, 60, multiplier=2)
+
+    result = list(itertools.islice(gen, 8))
+    assert result == [1, 2, 4, 8, 16, 32, 60, 60]
+
+
+@mock.patch('time.sleep', autospec=True)
+@mock.patch(
+    'google.api_core.datetime_helpers.utcnow',
+    return_value=datetime.datetime.min,
+    autospec=True)
+def test_retry_target_success(utcnow, sleep):
+    predicate = retry.if_exception_type(ValueError)
+    call_count = [0]
+
+    def target():
+        call_count[0] += 1
+        if call_count[0] < 3:
+            raise ValueError()
+        return 42
+
+    result = retry.retry_target(target, predicate, range(10), None)
+
+    assert result == 42
+    assert call_count[0] == 3
+    sleep.assert_has_calls([mock.call(0), mock.call(1)])
+
+
+@mock.patch('time.sleep', autospec=True)
+@mock.patch(
+    'google.api_core.datetime_helpers.utcnow',
+    return_value=datetime.datetime.min,
+    autospec=True)
+def test_retry_target_w_on_error(utcnow, sleep):
+    predicate = retry.if_exception_type(ValueError)
+    call_count = {'target': 0}
+    to_raise = ValueError()
+
+    def target():
+        call_count['target'] += 1
+        if call_count['target'] < 3:
+            raise to_raise
+        return 42
+
+    on_error = mock.Mock()
+
+    result = retry.retry_target(
+        target, predicate, range(10), None, on_error=on_error)
+
+    assert result == 42
+    assert call_count['target'] == 3
+
+    on_error.assert_has_calls([mock.call(to_raise), mock.call(to_raise)])
+    sleep.assert_has_calls([mock.call(0), mock.call(1)])
+
+
+@mock.patch('time.sleep', autospec=True)
+@mock.patch(
+    'google.api_core.datetime_helpers.utcnow',
+    return_value=datetime.datetime.min,
+    autospec=True)
+def test_retry_target_non_retryable_error(utcnow, sleep):
+    predicate = retry.if_exception_type(ValueError)
+    exception = TypeError()
+    target = mock.Mock(side_effect=exception)
+
+    with pytest.raises(TypeError) as exc_info:
+        retry.retry_target(target, predicate, range(10), None)
+
+    assert exc_info.value == exception
+    sleep.assert_not_called()
+
+
+@mock.patch('time.sleep', autospec=True)
+@mock.patch(
+    'google.api_core.datetime_helpers.utcnow', autospec=True)
+def test_retry_target_deadline_exceeded(utcnow, sleep):
+    predicate = retry.if_exception_type(ValueError)
+    exception = ValueError('meep')
+    target = mock.Mock(side_effect=exception)
+    # Setup the timeline so that the first call takes 5 seconds but the second
+    # call takes 6, which puts the retry over the deadline.
+    utcnow.side_effect = [
+        # The first call to utcnow establishes the start of the timeline.
+        datetime.datetime.min,
+        datetime.datetime.min + datetime.timedelta(seconds=5),
+        datetime.datetime.min + datetime.timedelta(seconds=11)]
+
+    with pytest.raises(exceptions.RetryError) as exc_info:
+        retry.retry_target(target, predicate, range(10), deadline=10)
+
+    assert exc_info.value.cause == exception
+    assert exc_info.match('Deadline of 10.0s exceeded')
+    assert exc_info.match('last exception: meep')
+    assert target.call_count == 2
+
+
+def test_retry_target_bad_sleep_generator():
+    with pytest.raises(ValueError, match='Sleep generator'):
+        retry.retry_target(
+            mock.sentinel.target, mock.sentinel.predicate, [], None)
+
+
+class TestRetry(object):
+    def test_constructor_defaults(self):
+        retry_ = retry.Retry()
+        assert retry_._predicate == retry.if_transient_error
+        assert retry_._initial == 1
+        assert retry_._maximum == 60
+        assert retry_._multiplier == 2
+        assert retry_._deadline == 120
+
+    def test_constructor_options(self):
+        retry_ = retry.Retry(
+            predicate=mock.sentinel.predicate,
+            initial=1,
+            maximum=2,
+            multiplier=3,
+            deadline=4,
+        )
+        assert retry_._predicate == mock.sentinel.predicate
+        assert retry_._initial == 1
+        assert retry_._maximum == 2
+        assert retry_._multiplier == 3
+        assert retry_._deadline == 4
+
+    def test_with_deadline(self):
+        retry_ = retry.Retry()
+        new_retry = retry_.with_deadline(42)
+        assert retry_ is not new_retry
+        assert new_retry._deadline == 42
+
+    def test_with_predicate(self):
+        retry_ = retry.Retry()
+        new_retry = retry_.with_predicate(mock.sentinel.predicate)
+        assert retry_ is not new_retry
+        assert new_retry._predicate == mock.sentinel.predicate
+
+    def test_with_delay_noop(self):
+        retry_ = retry.Retry()
+        new_retry = retry_.with_delay()
+        assert retry_ is not new_retry
+        assert new_retry._initial == retry_._initial
+        assert new_retry._maximum == retry_._maximum
+        assert new_retry._multiplier == retry_._multiplier
+
+    def test_with_delay(self):
+        retry_ = retry.Retry()
+        new_retry = retry_.with_delay(
+            initial=1, maximum=2, multiplier=3)
+        assert retry_ is not new_retry
+        assert new_retry._initial == 1
+        assert new_retry._maximum == 2
+        assert new_retry._multiplier == 3
+
+    def test___str__(self):
+        retry_ = retry.Retry()
+        assert re.match((
+            r'<Retry predicate=<function.*?if_exception_type.*?>, '
+            r'initial=1.0, maximum=60.0, multiplier=2.0, deadline=120.0>'),
+            str(retry_))
+
+    @mock.patch('time.sleep', autospec=True)
+    def test___call___and_execute_success(self, sleep):
+        retry_ = retry.Retry()
+        target = mock.Mock(spec=['__call__'], return_value=42)
+        # __name__ is needed by functools.partial.
+        target.__name__ = 'target'
+
+        decorated = retry_(target)
+        target.assert_not_called()
+
+        result = decorated('meep')
+
+        assert result == 42
+        target.assert_called_once_with('meep')
+        sleep.assert_not_called()
+
+    # Make uniform return half of its maximum, which will be the calculated
+    # sleep time.
+    @mock.patch(
+        'random.uniform', autospec=True, side_effect=lambda m, n: n/2.0)
+    @mock.patch('time.sleep', autospec=True)
+    def test___call___and_execute_retry(self, sleep, uniform):
+
+        on_error = mock.Mock(spec=['__call__'], side_effect=[None])
+        retry_ = retry.Retry(
+            predicate=retry.if_exception_type(ValueError),
+        )
+
+        target = mock.Mock(spec=['__call__'], side_effect=[ValueError(), 42])
+        # __name__ is needed by functools.partial.
+        target.__name__ = 'target'
+
+        decorated = retry_(target, on_error=on_error)
+        target.assert_not_called()
+
+        result = decorated('meep')
+
+        assert result == 42
+        assert target.call_count == 2
+        target.assert_has_calls([mock.call('meep'), mock.call('meep')])
+        sleep.assert_called_once_with(retry_._initial)
+        assert on_error.call_count == 1
diff --git a/tests/unit/test_timeout.py b/tests/unit/test_timeout.py
new file mode 100644
index 0000000..40caef4
--- /dev/null
+++ b/tests/unit/test_timeout.py
@@ -0,0 +1,132 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+import datetime
+import itertools
+
+import mock
+
+from google.api_core import timeout
+
+
+def test__exponential_timeout_generator_base_2():
+    gen = timeout._exponential_timeout_generator(
+        1.0, 60.0, 2.0, deadline=None)
+
+    result = list(itertools.islice(gen, 8))
+    assert result == [1, 2, 4, 8, 16, 32, 60, 60]
+
+
+@mock.patch('google.api_core.datetime_helpers.utcnow', autospec=True)
+def test__exponential_timeout_generator_base_deadline(utcnow):
+    # Make each successive call to utcnow() advance one second.
+    utcnow.side_effect = [
+        datetime.datetime.min + datetime.timedelta(seconds=n)
+        for n in range(15)]
+
+    gen = timeout._exponential_timeout_generator(
+        1.0, 60.0, 2.0, deadline=30.0)
+
+    result = list(itertools.islice(gen, 14))
+    # Should grow until the cumulative time is > 30s, then start decreasing as
+    # the cumulative time approaches 60s.
+    assert result == [1, 2, 4, 8, 16, 24, 23, 22, 21, 20, 19, 18, 17, 16]
+
+
+class TestConstantTimeout(object):
+
+    def test_constructor(self):
+        timeout_ = timeout.ConstantTimeout()
+        assert timeout_._timeout is None
+
+    def test_constructor_args(self):
+        timeout_ = timeout.ConstantTimeout(42.0)
+        assert timeout_._timeout == 42.0
+
+    def test___str__(self):
+        timeout_ = timeout.ConstantTimeout(1)
+        assert str(timeout_) == '<ConstantTimeout timeout=1.0>'
+
+    def test_apply(self):
+        target = mock.Mock(spec=['__call__', '__name__'], __name__='target')
+        timeout_ = timeout.ConstantTimeout(42.0)
+        wrapped = timeout_(target)
+
+        wrapped()
+
+        target.assert_called_once_with(timeout=42.0)
+
+    def test_apply_passthrough(self):
+        target = mock.Mock(spec=['__call__', '__name__'], __name__='target')
+        timeout_ = timeout.ConstantTimeout(42.0)
+        wrapped = timeout_(target)
+
+        wrapped(1, 2, meep='moop')
+
+        target.assert_called_once_with(1, 2, meep='moop', timeout=42.0)
+
+
+class TestExponentialTimeout(object):
+
+    def test_constructor(self):
+        timeout_ = timeout.ExponentialTimeout()
+        assert timeout_._initial == timeout._DEFAULT_INITIAL_TIMEOUT
+        assert timeout_._maximum == timeout._DEFAULT_MAXIMUM_TIMEOUT
+        assert timeout_._multiplier == timeout._DEFAULT_TIMEOUT_MULTIPLIER
+        assert timeout_._deadline == timeout._DEFAULT_DEADLINE
+
+    def test_constructor_args(self):
+        timeout_ = timeout.ExponentialTimeout(1, 2, 3, 4)
+        assert timeout_._initial == 1
+        assert timeout_._maximum == 2
+        assert timeout_._multiplier == 3
+        assert timeout_._deadline == 4
+
+    def test_with_timeout(self):
+        original_timeout = timeout.ExponentialTimeout()
+        timeout_ = original_timeout.with_deadline(42)
+        assert original_timeout is not timeout_
+        assert timeout_._initial == timeout._DEFAULT_INITIAL_TIMEOUT
+        assert timeout_._maximum == timeout._DEFAULT_MAXIMUM_TIMEOUT
+        assert timeout_._multiplier == timeout._DEFAULT_TIMEOUT_MULTIPLIER
+        assert timeout_._deadline == 42
+
+    def test___str__(self):
+        timeout_ = timeout.ExponentialTimeout(1, 2, 3, 4)
+        assert str(timeout_) == (
+            '<ExponentialTimeout initial=1.0, maximum=2.0, multiplier=3.0, '
+            'deadline=4.0>')
+
+    def test_apply(self):
+        target = mock.Mock(spec=['__call__', '__name__'], __name__='target')
+        timeout_ = timeout.ExponentialTimeout(1, 10, 2)
+        wrapped = timeout_(target)
+
+        wrapped()
+        target.assert_called_with(timeout=1)
+
+        wrapped()
+        target.assert_called_with(timeout=2)
+
+        wrapped()
+        target.assert_called_with(timeout=4)
+
+    def test_apply_passthrough(self):
+        target = mock.Mock(spec=['__call__', '__name__'], __name__='target')
+        timeout_ = timeout.ExponentialTimeout(42.0, 100, 2)
+        wrapped = timeout_(target)
+
+        wrapped(1, 2, meep='moop')
+
+        target.assert_called_once_with(1, 2, meep='moop', timeout=42.0)