From b263a7a71e3aac174808cc48b15d89b6e7bb4ea0 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 14 Aug 2026 06:27:04 -0400 Subject: [PATCH 1/8] test(api-core): add robust grpc server test for otel integration --- .../tests/unit/test_grpc_helpers_otel.py | 94 +++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index bc6b5f4b9f4d..1b51dac64095 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -16,16 +16,49 @@ import sys import types +from concurrent import futures from unittest import mock import pytest try: - from google.api_core import grpc_helpers - - HAS_GRPC_HELPERS = True + import grpc except ImportError: - HAS_GRPC_HELPERS = False + pytest.skip("No GRPC", allow_module_level=True) + +from google.api_core import grpc_helpers + + +class GenericEchoHandler(grpc.GenericRpcHandler): + """ + A generic handler that routes low-level gRPC calls without requiring + compiled protobuf stubs. + """ + + def service(self, handler_call_details): + if handler_call_details.method == "/DummyService/Echo": + # Return a simple Unary-Unary handler that echoes back the request + return grpc.unary_unary_rpc_method_handler( + lambda request, context: request, # Echo logic + request_deserializer=lambda x: x, # Pass raw bytes through + response_serializer=lambda x: x, # Pass raw bytes through + ) + return None + + +@pytest.fixture(scope="module") +def local_grpc_server(): + """Starts a local generic gRPC server on an open port.""" + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + server.add_generic_rpc_handlers((GenericEchoHandler(),)) + + # Bind to an ephemeral port (port 0 lets the OS assign one) + port = server.add_insecure_port("localhost:0") + server.start() + + yield f"localhost:{port}" + + server.stop(None) @pytest.fixture @@ -56,7 +89,6 @@ def mock_otel_grpc(monkeypatch): pytest.param(False, "true", False, id="not_installed_fails_open"), ], ) -@pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") def test_create_channel_otel_combos( monkeypatch, mock_otel_grpc, @@ -108,7 +140,6 @@ def test_create_channel_otel_combos( ], ids=["dict", "object"], ) -@pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") def test_create_channel_with_custom_tracer_provider( monkeypatch, mock_otel_grpc, config_factory ): @@ -130,3 +161,54 @@ def test_create_channel_with_custom_tracer_provider( mock_otel_grpc.client_interceptor.assert_called_once_with( tracer_provider=mock_tracer_provider ) + + +def test_otel_integration_with_fake_endpoint(local_grpc_server, monkeypatch): + """Verify OpenTelemetry integration with a real local gRPC server.""" + try: + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + InMemorySpanExporter, + SimpleSpanProcessor, + ) + except ImportError as e: + pytest.skip(f"opentelemetry-sdk not installed or import failed: {e}") + + # A) Setup OpenTelemetry with an In-Memory Exporter + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + + # B) Enable tracing via environment variable + monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "True") + + # C) Mock secure_channel to return an insecure channel + # This is needed because local_grpc_server is insecure but create_channel defaults to secure. + def mock_secure(*args, **kwargs): + return grpc.insecure_channel(args[0]) + + monkeypatch.setattr(grpc, "secure_channel", mock_secure) + + # D) Call the code under test + channel = grpc_helpers.create_channel( + local_grpc_server, configuration={"tracer_provider": provider} + ) + + # E) Make a low-level generic call + method_callable = channel.unary_unary( + "/DummyService/Echo", + request_serializer=lambda x: x, + response_deserializer=lambda x: x, + ) + + payload = b"ping-test" + response = method_callable(payload) + + # F) Assertions + assert response == payload # Server responded correctly + + spans = exporter.get_finished_spans() + assert len(spans) > 0, "No spans were recorded by OpenTelemetry!" + + span_names = [s.name for s in spans] + assert any("DummyService" in name for name in span_names) From 7c1d1eaea772091d469f71df748babb6671d69df Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 14 Aug 2026 07:35:10 -0400 Subject: [PATCH 2/8] fix(api-core): use otel_grpc.intercept_channel, raise bounds, and refine tests --- packages/google-api-core/google/api_core/grpc_helpers.py | 1 - packages/google-api-core/noxfile.py | 2 +- packages/google-api-core/pyproject.toml | 7 +++++-- packages/google-api-core/testing/constraints-3.10.txt | 4 +++- .../testing/constraints-async-rest-3.10.txt | 4 +++- .../google-api-core/tests/unit/test_grpc_helpers_otel.py | 4 ++-- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/google-api-core/google/api_core/grpc_helpers.py b/packages/google-api-core/google/api_core/grpc_helpers.py index cfcb0552b8e1..21a598c3055d 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers.py +++ b/packages/google-api-core/google/api_core/grpc_helpers.py @@ -394,7 +394,6 @@ def create_channel( feature_key="tracer_provider", configuration=configuration, ) - if is_tracing_enabled: try: import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] diff --git a/packages/google-api-core/noxfile.py b/packages/google-api-core/noxfile.py index 673b91043bd6..c5975c35c25f 100644 --- a/packages/google-api-core/noxfile.py +++ b/packages/google-api-core/noxfile.py @@ -217,7 +217,7 @@ def default( install_extras = [] if install_grpc: # Note: The extra is called `grpc` and not `grpcio`. - install_extras.append("grpc") + install_extras.extend(["grpc", "tracing", "testing"]) constraints_dir = str(CURRENT_DIRECTORY / "testing") if install_async_rest: diff --git a/packages/google-api-core/pyproject.toml b/packages/google-api-core/pyproject.toml index 58ce70109648..6168b6f79608 100644 --- a/packages/google-api-core/pyproject.toml +++ b/packages/google-api-core/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "proto-plus >= 1.26.1, < 2.0.0", "google-auth >= 2.14.1, < 3.0.0", "requests >= 2.33.0, < 3.0.0", - "opentelemetry-api >= 1.27.0, < 2.0.0", + "opentelemetry-api >= 1.44.0, < 2.0.0", ] dynamic = ["version"] @@ -66,7 +66,10 @@ grpc = [ "grpcio-status >= 1.75.1, < 2.0.0; python_version >= '3.14'", ] tracing = [ - "opentelemetry-instrumentation-grpc >= 0.46b0, < 1.0.0", + "opentelemetry-instrumentation-grpc >= 0.65b0, < 1.0.0", +] +testing = [ + "opentelemetry-sdk >= 1.44.0, < 2.0.0", ] diff --git a/packages/google-api-core/testing/constraints-3.10.txt b/packages/google-api-core/testing/constraints-3.10.txt index 4cb9760152c6..786cb13d5746 100644 --- a/packages/google-api-core/testing/constraints-3.10.txt +++ b/packages/google-api-core/testing/constraints-3.10.txt @@ -12,4 +12,6 @@ requests==2.33.0 grpcio==1.59.0 grpcio-status==1.59.0 proto-plus==1.26.1 -opentelemetry-api==1.27.0 +opentelemetry-api==1.44.0 +opentelemetry-sdk==1.44.0 +opentelemetry-instrumentation-grpc==0.65b0 diff --git a/packages/google-api-core/testing/constraints-async-rest-3.10.txt b/packages/google-api-core/testing/constraints-async-rest-3.10.txt index bd2beec5f247..b400c2224225 100644 --- a/packages/google-api-core/testing/constraints-async-rest-3.10.txt +++ b/packages/google-api-core/testing/constraints-async-rest-3.10.txt @@ -13,4 +13,6 @@ grpcio==1.59.0 grpcio-status==1.59.0 proto-plus==1.26.1 aiohttp==3.13.4 -opentelemetry-api==1.27.0 +opentelemetry-api==1.44.0 +opentelemetry-sdk==1.44.0 +opentelemetry-instrumentation-grpc==0.65b0 diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index 1b51dac64095..0b4e04d0a0e5 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -167,9 +167,9 @@ def test_otel_integration_with_fake_endpoint(local_grpc_server, monkeypatch): """Verify OpenTelemetry integration with a real local gRPC server.""" try: from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import ( + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, - SimpleSpanProcessor, ) except ImportError as e: pytest.skip(f"opentelemetry-sdk not installed or import failed: {e}") From 069174ba483e90a7bc4f2dc3221df66c76632dd5 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 17 Aug 2026 06:45:20 -0400 Subject: [PATCH 3/8] feat: add robust integration tests and sad paths for OTel tracing --- .../tests/unit/test_grpc_helpers_otel.py | 81 ++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index 0b4e04d0a0e5..bf8bc9039e1c 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -43,6 +43,16 @@ def service(self, handler_call_details): request_deserializer=lambda x: x, # Pass raw bytes through response_serializer=lambda x: x, # Pass raw bytes through ) + elif handler_call_details.method == "/DummyService/Error": + def error_behavior(request, context): + context.set_code(grpc.StatusCode.INTERNAL) + context.set_details("Intentional test error") + return b"" + return grpc.unary_unary_rpc_method_handler( + error_behavior, + request_deserializer=lambda x: x, + response_serializer=lambda x: x, + ) return None @@ -163,7 +173,15 @@ def test_create_channel_with_custom_tracer_provider( ) -def test_otel_integration_with_fake_endpoint(local_grpc_server, monkeypatch): +@pytest.mark.parametrize( + "config_factory", + [ + lambda tp: {"tracer_provider": tp}, + lambda tp: types.SimpleNamespace(tracer_provider=tp), + ], + ids=["dict", "object"], +) +def test_otel_integration_with_fake_endpoint(local_grpc_server, monkeypatch, config_factory): """Verify OpenTelemetry integration with a real local gRPC server.""" try: from opentelemetry.sdk.trace import TracerProvider @@ -179,6 +197,8 @@ def test_otel_integration_with_fake_endpoint(local_grpc_server, monkeypatch): provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(exporter)) + config = config_factory(provider) + # B) Enable tracing via environment variable monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "True") @@ -191,7 +211,7 @@ def mock_secure(*args, **kwargs): # D) Call the code under test channel = grpc_helpers.create_channel( - local_grpc_server, configuration={"tracer_provider": provider} + local_grpc_server, configuration=config ) # E) Make a low-level generic call @@ -212,3 +232,60 @@ def mock_secure(*args, **kwargs): span_names = [s.name for s in spans] assert any("DummyService" in name for name in span_names) + + +def test_otel_integration_with_fake_endpoint_error(local_grpc_server, monkeypatch): + """Verify OpenTelemetry integration records errors correctly.""" + try: + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + except ImportError as e: + pytest.skip(f"opentelemetry-sdk not installed or import failed: {e}") + + # A) Setup OpenTelemetry with an In-Memory Exporter + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + + # B) Enable tracing via environment variable + monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "True") + + # C) Mock secure_channel to return an insecure channel + def mock_secure(*args, **kwargs): + return grpc.insecure_channel(args[0]) + + monkeypatch.setattr(grpc, "secure_channel", mock_secure) + + # D) Call the code under test + channel = grpc_helpers.create_channel( + local_grpc_server, configuration={"tracer_provider": provider} + ) + + # E) Make a low-level generic call that triggers an error + method_callable = channel.unary_unary( + "/DummyService/Error", + request_serializer=lambda x: x, + response_deserializer=lambda x: x, + ) + + payload = b"ping-test" + + # Expect a gRPC error + with pytest.raises(grpc.RpcError) as excinfo: + method_callable(payload) + + # F) Assertions + spans = exporter.get_finished_spans() + assert len(spans) > 0, "No spans were recorded by OpenTelemetry!" + + # Verify that the span recorded the error + # The exact way OTel records errors might vary by version, but status should be ERROR + # or it should have error attributes. + error_spans = [s for s in spans if not s.status.is_ok] + assert len(error_spans) > 0, "No error spans recorded!" + + span = error_spans[0] + assert "DummyService" in span.name From 25d2a61e742b365b7d8734dc38206ac4d386d9e2 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 17 Aug 2026 08:12:41 -0400 Subject: [PATCH 4/8] refactor: use fixture for OTel in-memory setup in tests --- .../tests/unit/test_grpc_helpers_otel.py | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index bf8bc9039e1c..73d763612e5a 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -91,6 +91,26 @@ def mock_otel_grpc(monkeypatch): return mock_otel_grpc +@pytest.fixture +def otel_setup_in_memory(): + """Fixture to set up in-memory OTel exporting. Skips test if SDK is missing.""" + try: + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + except ImportError as e: + pytest.skip(f"opentelemetry-sdk not installed or import failed: {e}") + + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + + return provider, exporter + + + @pytest.mark.parametrize( "is_otel_installed, tracing_env_var_value, expect_otel_interceptor", [ @@ -181,21 +201,9 @@ def test_create_channel_with_custom_tracer_provider( ], ids=["dict", "object"], ) -def test_otel_integration_with_fake_endpoint(local_grpc_server, monkeypatch, config_factory): +def test_otel_integration_with_fake_endpoint(local_grpc_server, monkeypatch, config_factory, otel_setup_in_memory): """Verify OpenTelemetry integration with a real local gRPC server.""" - try: - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import SimpleSpanProcessor - from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, - ) - except ImportError as e: - pytest.skip(f"opentelemetry-sdk not installed or import failed: {e}") - - # A) Setup OpenTelemetry with an In-Memory Exporter - exporter = InMemorySpanExporter() - provider = TracerProvider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) + provider, exporter = otel_setup_in_memory config = config_factory(provider) @@ -234,21 +242,9 @@ def mock_secure(*args, **kwargs): assert any("DummyService" in name for name in span_names) -def test_otel_integration_with_fake_endpoint_error(local_grpc_server, monkeypatch): +def test_otel_integration_with_fake_endpoint_error(local_grpc_server, monkeypatch, otel_setup_in_memory): """Verify OpenTelemetry integration records errors correctly.""" - try: - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import SimpleSpanProcessor - from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, - ) - except ImportError as e: - pytest.skip(f"opentelemetry-sdk not installed or import failed: {e}") - - # A) Setup OpenTelemetry with an In-Memory Exporter - exporter = InMemorySpanExporter() - provider = TracerProvider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) + provider, exporter = otel_setup_in_memory # B) Enable tracing via environment variable monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "True") From 97971373d082e7d8eb72a934b60a6dd16cff84ec Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 17 Aug 2026 08:30:31 -0400 Subject: [PATCH 5/8] refactor: extract insecure channel boilerplate to fixture and parametrize error tests --- .../tests/unit/test_grpc_helpers_otel.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index 73d763612e5a..17886039c739 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -110,6 +110,16 @@ def otel_setup_in_memory(): return provider, exporter +@pytest.fixture +def insecure_channel_patch(monkeypatch): + """Mocks grpc.secure_channel to return an insecure channel for testing.""" + def mock_secure(*args, **kwargs): + return grpc.insecure_channel(args[0]) + + monkeypatch.setattr(grpc, "secure_channel", mock_secure) + + + @pytest.mark.parametrize( "is_otel_installed, tracing_env_var_value, expect_otel_interceptor", @@ -201,22 +211,16 @@ def test_create_channel_with_custom_tracer_provider( ], ids=["dict", "object"], ) -def test_otel_integration_with_fake_endpoint(local_grpc_server, monkeypatch, config_factory, otel_setup_in_memory): +def test_otel_integration_with_fake_endpoint( + local_grpc_server, monkeypatch, config_factory, otel_setup_in_memory, insecure_channel_patch +): """Verify OpenTelemetry integration with a real local gRPC server.""" provider, exporter = otel_setup_in_memory - config = config_factory(provider) # B) Enable tracing via environment variable monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "True") - # C) Mock secure_channel to return an insecure channel - # This is needed because local_grpc_server is insecure but create_channel defaults to secure. - def mock_secure(*args, **kwargs): - return grpc.insecure_channel(args[0]) - - monkeypatch.setattr(grpc, "secure_channel", mock_secure) - # D) Call the code under test channel = grpc_helpers.create_channel( local_grpc_server, configuration=config @@ -242,22 +246,27 @@ def mock_secure(*args, **kwargs): assert any("DummyService" in name for name in span_names) -def test_otel_integration_with_fake_endpoint_error(local_grpc_server, monkeypatch, otel_setup_in_memory): +@pytest.mark.parametrize( + "config_factory", + [ + lambda tp: {"tracer_provider": tp}, + lambda tp: types.SimpleNamespace(tracer_provider=tp), + ], + ids=["dict", "object"], +) +def test_otel_integration_with_fake_endpoint_error( + local_grpc_server, monkeypatch, config_factory, otel_setup_in_memory, insecure_channel_patch +): """Verify OpenTelemetry integration records errors correctly.""" provider, exporter = otel_setup_in_memory + config = config_factory(provider) # B) Enable tracing via environment variable monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "True") - # C) Mock secure_channel to return an insecure channel - def mock_secure(*args, **kwargs): - return grpc.insecure_channel(args[0]) - - monkeypatch.setattr(grpc, "secure_channel", mock_secure) - # D) Call the code under test channel = grpc_helpers.create_channel( - local_grpc_server, configuration={"tracer_provider": provider} + local_grpc_server, configuration=config ) # E) Make a low-level generic call that triggers an error From 6a9735297e635964ced442c1e4dbf8bbc9cde64a Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 17 Aug 2026 09:10:33 -0400 Subject: [PATCH 6/8] refactor: rename fixtures for better self-documentation --- .../tests/unit/test_grpc_helpers_otel.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index 17886039c739..e2e690e4f1c3 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -57,7 +57,7 @@ def error_behavior(request, context): @pytest.fixture(scope="module") -def local_grpc_server(): +def fake_grpc_endpoint_server(): """Starts a local generic gRPC server on an open port.""" server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) server.add_generic_rpc_handlers((GenericEchoHandler(),)) @@ -72,7 +72,7 @@ def local_grpc_server(): @pytest.fixture -def mock_otel_grpc(monkeypatch): +def mock_otel_package_imports(monkeypatch): """Fixture to mock OpenTelemetry gRPC hierarchy.""" mock_otel = mock.Mock() mock_otel_grpc = mock_otel.instrumentation.grpc @@ -92,7 +92,7 @@ def mock_otel_grpc(monkeypatch): @pytest.fixture -def otel_setup_in_memory(): +def real_otel_sdk_in_memory(): """Fixture to set up in-memory OTel exporting. Skips test if SDK is missing.""" try: from opentelemetry.sdk.trace import TracerProvider @@ -131,7 +131,7 @@ def mock_secure(*args, **kwargs): ) def test_create_channel_otel_combos( monkeypatch, - mock_otel_grpc, + mock_otel_package_imports, is_otel_installed, tracing_env_var_value, expect_otel_interceptor, @@ -144,7 +144,7 @@ def test_create_channel_otel_combos( monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", None) mock_channel = "raw_channel" - mock_otel_grpc.intercept_channel.side_effect = lambda ch, inc: f"wrapped_{ch}" + mock_otel_package_imports.intercept_channel.side_effect = lambda ch, inc: f"wrapped_{ch}" with ( mock.patch( @@ -161,14 +161,14 @@ def test_create_channel_otel_combos( mock_secure_channel.assert_called_once() if expect_otel_interceptor: - mock_otel_grpc.client_interceptor.assert_called_once() - mock_otel_grpc.intercept_channel.assert_called_once_with( - mock_channel, mock_otel_grpc.client_interceptor.return_value + mock_otel_package_imports.client_interceptor.assert_called_once() + mock_otel_package_imports.intercept_channel.assert_called_once_with( + mock_channel, mock_otel_package_imports.client_interceptor.return_value ) assert channel == f"wrapped_{mock_channel}" else: # OTel should NOT have been called - mock_otel_grpc.intercept_channel.assert_not_called() + mock_otel_package_imports.intercept_channel.assert_not_called() assert channel == mock_channel @@ -181,7 +181,7 @@ def test_create_channel_otel_combos( ids=["dict", "object"], ) def test_create_channel_with_custom_tracer_provider( - monkeypatch, mock_otel_grpc, config_factory + monkeypatch, mock_otel_package_imports, config_factory ): """Verify that create_channel passes custom tracer_provider to OTel interceptor.""" @@ -198,7 +198,7 @@ def test_create_channel_with_custom_tracer_provider( ): grpc_helpers.create_channel("localhost:1234", configuration=config) - mock_otel_grpc.client_interceptor.assert_called_once_with( + mock_otel_package_imports.client_interceptor.assert_called_once_with( tracer_provider=mock_tracer_provider ) @@ -212,10 +212,10 @@ def test_create_channel_with_custom_tracer_provider( ids=["dict", "object"], ) def test_otel_integration_with_fake_endpoint( - local_grpc_server, monkeypatch, config_factory, otel_setup_in_memory, insecure_channel_patch + fake_grpc_endpoint_server, monkeypatch, config_factory, real_otel_sdk_in_memory, insecure_channel_patch ): """Verify OpenTelemetry integration with a real local gRPC server.""" - provider, exporter = otel_setup_in_memory + provider, exporter = real_otel_sdk_in_memory config = config_factory(provider) # B) Enable tracing via environment variable @@ -223,7 +223,7 @@ def test_otel_integration_with_fake_endpoint( # D) Call the code under test channel = grpc_helpers.create_channel( - local_grpc_server, configuration=config + fake_grpc_endpoint_server, configuration=config ) # E) Make a low-level generic call @@ -255,21 +255,21 @@ def test_otel_integration_with_fake_endpoint( ids=["dict", "object"], ) def test_otel_integration_with_fake_endpoint_error( - local_grpc_server, monkeypatch, config_factory, otel_setup_in_memory, insecure_channel_patch + fake_grpc_endpoint_server, monkeypatch, config_factory, real_otel_sdk_in_memory, insecure_channel_patch ): """Verify OpenTelemetry integration records errors correctly.""" - provider, exporter = otel_setup_in_memory + provider, exporter = real_otel_sdk_in_memory config = config_factory(provider) - # B) Enable tracing via environment variable + # Enable tracing via environment variable monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "True") - # D) Call the code under test + # Call the code under test channel = grpc_helpers.create_channel( - local_grpc_server, configuration=config + fake_grpc_endpoint_server, configuration=config ) - # E) Make a low-level generic call that triggers an error + # Make a low-level generic call that triggers an error method_callable = channel.unary_unary( "/DummyService/Error", request_serializer=lambda x: x, @@ -282,7 +282,7 @@ def test_otel_integration_with_fake_endpoint_error( with pytest.raises(grpc.RpcError) as excinfo: method_callable(payload) - # F) Assertions + # Assertions spans = exporter.get_finished_spans() assert len(spans) > 0, "No spans were recorded by OpenTelemetry!" From d6401b348cedf6ef6e50a7a199477daa58f9eb91 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 17 Aug 2026 09:30:45 -0400 Subject: [PATCH 7/8] docs: clean up step labels in integration tests --- .../google-api-core/tests/unit/test_grpc_helpers_otel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index e2e690e4f1c3..b8be4cb52915 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -218,15 +218,15 @@ def test_otel_integration_with_fake_endpoint( provider, exporter = real_otel_sdk_in_memory config = config_factory(provider) - # B) Enable tracing via environment variable + # Enable tracing via environment variable monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "True") - # D) Call the code under test + # Call the code under test channel = grpc_helpers.create_channel( fake_grpc_endpoint_server, configuration=config ) - # E) Make a low-level generic call + # Make a low-level generic call method_callable = channel.unary_unary( "/DummyService/Echo", request_serializer=lambda x: x, @@ -236,7 +236,7 @@ def test_otel_integration_with_fake_endpoint( payload = b"ping-test" response = method_callable(payload) - # F) Assertions + # Assertions assert response == payload # Server responded correctly spans = exporter.get_finished_spans() From 2c110ee9ba2a30dfee70d427b8e293f5814d225c Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 17 Aug 2026 11:02:23 -0400 Subject: [PATCH 8/8] refactor: remove redundant unit test in favor of integration test --- .../tests/unit/test_grpc_helpers_otel.py | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index b8be4cb52915..31cd52807d82 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -172,35 +172,6 @@ def test_create_channel_otel_combos( assert channel == mock_channel -@pytest.mark.parametrize( - "config_factory", - [ - lambda tp: {"tracer_provider": tp}, - lambda tp: types.SimpleNamespace(tracer_provider=tp), - ], - ids=["dict", "object"], -) -def test_create_channel_with_custom_tracer_provider( - monkeypatch, mock_otel_package_imports, config_factory -): - """Verify that create_channel passes custom tracer_provider to OTel interceptor.""" - - mock_tracer_provider = mock.Mock() - config = config_factory(mock_tracer_provider) - - mock_channel = "raw_channel" - with ( - mock.patch("grpc.secure_channel", return_value=mock_channel), - ): - with mock.patch( - "google.api_core.grpc_helpers._create_composite_credentials", - return_value=mock.Mock(), - ): - grpc_helpers.create_channel("localhost:1234", configuration=config) - - mock_otel_package_imports.client_interceptor.assert_called_once_with( - tracer_provider=mock_tracer_provider - ) @pytest.mark.parametrize(