tests: add testing w/o 'grpc' installed (#289)

Closes #288.
diff --git a/noxfile.py b/noxfile.py
index 926f9f5..ac1bdd1 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -33,6 +33,7 @@
 nox.options.sessions = [
     "unit",
     "unit_grpc_gcp",
+    "unit_wo_grpc",
     "cover",
     "pytype",
     "mypy",
@@ -78,7 +79,7 @@
     session.run("black", *BLACK_EXCLUDES, *BLACK_PATHS)
 
 
-def default(session):
+def default(session, install_grpc=True):
     """Default unit test session.
 
     This is intended to be run **without** an interpreter set, so
@@ -92,7 +93,10 @@
 
     # Install all test dependencies, then install this package in-place.
     session.install("mock", "pytest", "pytest-cov")
-    session.install("-e", ".[grpc]", "-c", constraints_path)
+    if install_grpc:
+        session.install("-e", ".[grpc]", "-c", constraints_path)
+    else:
+        session.install("-e", ".", "-c", constraints_path)
 
     pytest_args = [
         "python",
@@ -140,6 +144,12 @@
     default(session)
 
 
+@nox.session(python=["3.6", "3.10"])
+def unit_wo_grpc(session):
+    """Run the unit test suite w/o grpcio installed"""
+    default(session, install_grpc=False)
+
+
 @nox.session(python="3.6")
 def lint_setup_py(session):
     """Verify that setup.py is valid (including RST check)."""
diff --git a/tests/asyncio/gapic/test_config_async.py b/tests/asyncio/gapic/test_config_async.py
index 1f6ea9e..dbb05d5 100644
--- a/tests/asyncio/gapic/test_config_async.py
+++ b/tests/asyncio/gapic/test_config_async.py
@@ -12,6 +12,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+
+import pytest
+
+try:
+    import grpc  # noqa: F401
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 from google.api_core import exceptions
 from google.api_core.gapic_v1 import config_async
 
diff --git a/tests/asyncio/gapic/test_method_async.py b/tests/asyncio/gapic/test_method_async.py
index e24fe44..1410747 100644
--- a/tests/asyncio/gapic/test_method_async.py
+++ b/tests/asyncio/gapic/test_method_async.py
@@ -14,10 +14,14 @@
 
 import datetime
 
-from grpc import aio
 import mock
 import pytest
 
+try:
+    from grpc import aio
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 from google.api_core import exceptions
 from google.api_core import gapic_v1
 from google.api_core import grpc_helpers_async
diff --git a/tests/asyncio/operations_v1/test_operations_async_client.py b/tests/asyncio/operations_v1/test_operations_async_client.py
index 3fb8427..47c3b4b 100644
--- a/tests/asyncio/operations_v1/test_operations_async_client.py
+++ b/tests/asyncio/operations_v1/test_operations_async_client.py
@@ -12,11 +12,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from grpc import aio
 import mock
 import pytest
 
-from google.api_core import grpc_helpers_async, operations_v1, page_iterator_async
+try:
+    from grpc import aio
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
+from google.api_core import grpc_helpers_async
+from google.api_core import operations_v1
+from google.api_core import page_iterator_async
 from google.longrunning import operations_pb2
 from google.protobuf import empty_pb2
 
diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py
index f0b6c5f..0413abf 100644
--- a/tests/asyncio/test_grpc_helpers_async.py
+++ b/tests/asyncio/test_grpc_helpers_async.py
@@ -12,10 +12,19 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import grpc
-from grpc import aio
 import mock
-import pytest
+import pytest  # noqa: I202
+
+try:
+    import grpc
+    from grpc import aio
+except ImportError:
+    grpc = aio = None
+
+
+if grpc is None:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 
 from google.api_core import exceptions
 from google.api_core import grpc_helpers_async
diff --git a/tests/asyncio/test_operation_async.py b/tests/asyncio/test_operation_async.py
index 342184f..886b1c8 100644
--- a/tests/asyncio/test_operation_async.py
+++ b/tests/asyncio/test_operation_async.py
@@ -16,6 +16,11 @@
 import mock
 import pytest
 
+try:
+    import grpc  # noqa: F401
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 from google.api_core import exceptions
 from google.api_core import operation_async
 from google.api_core import operations_v1
diff --git a/tests/unit/gapic/test_client_info.py b/tests/unit/gapic/test_client_info.py
index 64080ff..2ca5c40 100644
--- a/tests/unit/gapic/test_client_info.py
+++ b/tests/unit/gapic/test_client_info.py
@@ -12,6 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import pytest
+
+try:
+    import grpc  # noqa: F401
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 
 from google.api_core.gapic_v1 import client_info
 
diff --git a/tests/unit/gapic/test_config.py b/tests/unit/gapic/test_config.py
index 1c15261..5e42fde 100644
--- a/tests/unit/gapic/test_config.py
+++ b/tests/unit/gapic/test_config.py
@@ -12,6 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import pytest
+
+try:
+    import grpc  # noqa: F401
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 from google.api_core import exceptions
 from google.api_core.gapic_v1 import config
 
diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py
index e0ea57a..9778d23 100644
--- a/tests/unit/gapic/test_method.py
+++ b/tests/unit/gapic/test_method.py
@@ -15,6 +15,13 @@
 import datetime
 
 import mock
+import pytest
+
+try:
+    import grpc  # noqa: F401
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 
 from google.api_core import exceptions
 from google.api_core import retry
diff --git a/tests/unit/gapic/test_routing_header.py b/tests/unit/gapic/test_routing_header.py
index 77300e8..3037867 100644
--- a/tests/unit/gapic/test_routing_header.py
+++ b/tests/unit/gapic/test_routing_header.py
@@ -12,6 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import pytest
+
+try:
+    import grpc  # noqa: F401
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 
 from google.api_core.gapic_v1 import routing_header
 
diff --git a/tests/unit/operations_v1/test_operations_client.py b/tests/unit/operations_v1/test_operations_client.py
index 001b8fe..187f0be 100644
--- a/tests/unit/operations_v1/test_operations_client.py
+++ b/tests/unit/operations_v1/test_operations_client.py
@@ -12,6 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import pytest
+
+try:
+    import grpc  # noqa: F401
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 from google.api_core import grpc_helpers
 from google.api_core import operations_v1
 from google.api_core import page_iterator
diff --git a/tests/unit/test_bidi.py b/tests/unit/test_bidi.py
index 15994ee..7fb1620 100644
--- a/tests/unit/test_bidi.py
+++ b/tests/unit/test_bidi.py
@@ -17,10 +17,14 @@
 import queue
 import threading
 
-import grpc
 import mock
 import pytest
 
+try:
+    import grpc
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 from google.api_core import bidi
 from google.api_core import exceptions
 
diff --git a/tests/unit/test_client_info.py b/tests/unit/test_client_info.py
index f2274ec..f5eebfb 100644
--- a/tests/unit/test_client_info.py
+++ b/tests/unit/test_client_info.py
@@ -13,6 +13,11 @@
 # limitations under the License.
 
 
+try:
+    import grpc
+except ImportError:
+    grpc = None
+
 from google.api_core import client_info
 
 
@@ -20,7 +25,12 @@
     info = client_info.ClientInfo()
 
     assert info.python_version is not None
-    assert info.grpc_version is not None
+
+    if grpc is not None:
+        assert info.grpc_version is not None
+    else:
+        assert info.grpc_version is None
+
     assert info.api_core_version is not None
     assert info.gapic_version is None
     assert info.client_library_version is None
diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py
index e9709f2..95317db 100644
--- a/tests/unit/test_exceptions.py
+++ b/tests/unit/test_exceptions.py
@@ -15,10 +15,15 @@
 import http.client
 import json
 
-import grpc
 import mock
+import pytest
 import requests
 
+try:
+    import grpc
+except ImportError:
+    grpc = None
+
 from google.api_core import exceptions
 
 
@@ -151,6 +156,7 @@
     assert exception.errors == ["1", "2"]
 
 
+@pytest.mark.skipif(grpc is None, reason="No grpc")
 def test_from_grpc_status():
     message = "message"
     exception = exceptions.from_grpc_status(grpc.StatusCode.OUT_OF_RANGE, message)
@@ -162,6 +168,7 @@
     assert exception.errors == []
 
 
+@pytest.mark.skipif(grpc is None, reason="No grpc")
 def test_from_grpc_status_as_int():
     message = "message"
     exception = exceptions.from_grpc_status(11, message)
@@ -173,6 +180,7 @@
     assert exception.errors == []
 
 
+@pytest.mark.skipif(grpc is None, reason="No grpc")
 def test_from_grpc_status_with_errors_and_response():
     message = "message"
     response = mock.sentinel.response
@@ -187,6 +195,7 @@
     assert exception.response == response
 
 
+@pytest.mark.skipif(grpc is None, reason="No grpc")
 def test_from_grpc_status_unknown_code():
     message = "message"
     exception = exceptions.from_grpc_status(grpc.StatusCode.OK, message)
@@ -194,6 +203,7 @@
     assert exception.message == message
 
 
+@pytest.mark.skipif(grpc is None, reason="No grpc")
 def test_from_grpc_error():
     message = "message"
     error = mock.create_autospec(grpc.Call, instance=True)
@@ -211,6 +221,7 @@
     assert exception.response == error
 
 
+@pytest.mark.skipif(grpc is None, reason="No grpc")
 def test_from_grpc_error_non_call():
     message = "message"
     error = mock.create_autospec(grpc.RpcError, instance=True)
diff --git a/tests/unit/test_grpc_helpers.py b/tests/unit/test_grpc_helpers.py
index f584929..4c613a9 100644
--- a/tests/unit/test_grpc_helpers.py
+++ b/tests/unit/test_grpc_helpers.py
@@ -12,10 +12,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import grpc
 import mock
 import pytest
 
+try:
+    import grpc
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
+
 from google.api_core import exceptions
 from google.api_core import grpc_helpers
 import google.auth.credentials
diff --git a/tests/unit/test_operation.py b/tests/unit/test_operation.py
index 7a3e3c6..bafff8b 100644
--- a/tests/unit/test_operation.py
+++ b/tests/unit/test_operation.py
@@ -14,6 +14,12 @@
 
 
 import mock
+import pytest
+
+try:
+    import grpc  # noqa: F401
+except ImportError:
+    pytest.skip("No GRPC", allow_module_level=True)
 
 from google.api_core import exceptions
 from google.api_core import operation