Skip to content

Commit 41da3c6

Browse files
test: Add comprehensive tests for TapTestRunner streams parameter
- Add tests for TapTestRunner initialization with streams parameter - Add tests for run_sync_dry_run with streams parameter - Add tests for different stream sequence types (list, tuple, empty) - Add integration test for complete workflow with streams - Fix factory tests to include streams=None parameter expectations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent e542a35 commit 41da3c6

File tree

6 files changed

+174
-14
lines changed

6 files changed

+174
-14
lines changed

packages/tap-dummyjson/.pre-commit-config.yaml

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ ci:
55

66
repos:
77
- repo: https://github.com/pre-commit/pre-commit-hooks
8-
rev: v5.0.0
8+
rev: v6.0.0
99
hooks:
1010
- id: check-json
1111
exclude: |
@@ -18,21 +18,14 @@ repos:
1818
- id: trailing-whitespace
1919

2020
- repo: https://github.com/python-jsonschema/check-jsonschema
21-
rev: 0.30.0
21+
rev: 0.34.0
2222
hooks:
2323
- id: check-dependabot
2424
- id: check-github-workflows
2525

2626
- repo: https://github.com/astral-sh/ruff-pre-commit
27-
rev: v0.8.1
27+
rev: v0.13.0
2828
hooks:
29-
- id: ruff
29+
- id: ruff-check
3030
args: [--fix, --exit-non-zero-on-fix, --show-fixes]
3131
- id: ruff-format
32-
33-
- repo: https://github.com/pre-commit/mirrors-mypy
34-
rev: v1.13.0
35-
hooks:
36-
- id: mypy
37-
additional_dependencies:
38-
- types-requests

singer_sdk/tap_base.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def run_connection_test(self) -> bool:
261261
def run_sync_dry_run(
262262
self,
263263
dry_run_record_limit: int | None = 1,
264-
streams: t.Iterable[Stream] | None = None,
264+
streams: t.Iterable[Stream | str] | None = None,
265265
) -> bool:
266266
"""Run connection test.
267267
@@ -278,15 +278,24 @@ def run_sync_dry_run(
278278
if streams is None:
279279
streams = self.streams.values()
280280

281-
for stream in streams:
281+
selected_streams: list[Stream] = []
282+
283+
for stream_or_name in streams:
284+
stream = (
285+
self.streams[stream_or_name]
286+
if isinstance(stream_or_name, str)
287+
else stream_or_name
288+
)
289+
282290
if not stream.child_streams: # pragma: no branch
283291
# Initialize streams' record limits before beginning the sync test.
284292
stream.ABORT_AT_RECORD_COUNT = dry_run_record_limit
285293

286294
# Force selection of streams.
287295
stream.selected = True
296+
selected_streams.append(stream)
288297

289-
for stream in streams:
298+
for stream in selected_streams:
290299
if stream.parent_stream_type:
291300
self.logger.debug(
292301
"Child stream '%s' should be called by "

singer_sdk/testing/factory.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
)
1818

1919
if t.TYPE_CHECKING:
20+
from collections.abc import Sequence
21+
2022
from singer_sdk import Stream, Tap, Target
2123
from singer_sdk.testing.templates import (
2224
AttributeTestTemplate,
@@ -91,6 +93,7 @@ def new_test_class(
9193
include_stream_attribute_tests: bool = True,
9294
custom_suites: list | None = None,
9395
suite_config: SuiteConfig | None = None,
96+
streams: Sequence[str] | None = None,
9497
**kwargs: t.Any,
9598
) -> type[BaseTestClass]:
9699
"""Get a new test class.
@@ -102,6 +105,7 @@ def new_test_class(
102105
Include stream attribute tests in the test class.
103106
custom_suites: List of custom test suites to include in the test class.
104107
suite_config: SuiteConfig instance to be used when instantiating tests.
108+
streams: Streams to test.
105109
kwargs: Default arguments to be passed to tap on create.
106110
107111
Returns:
@@ -124,6 +128,7 @@ def new_test_class(
124128
tap_class=self.tap_class,
125129
config=self.config,
126130
suite_config=suite_config,
131+
streams=streams,
127132
**kwargs,
128133
)
129134

@@ -396,6 +401,7 @@ def get_tap_test_class(
396401
include_stream_attribute_tests: bool = True,
397402
custom_suites: list | None = None,
398403
suite_config: SuiteConfig | None = None,
404+
streams: Sequence[str] | None = None,
399405
**kwargs: t.Any,
400406
) -> type[BaseTestClass]:
401407
"""Get Tap Test Class.
@@ -408,6 +414,7 @@ def get_tap_test_class(
408414
include_stream_attribute_tests: Include Tap stream attribute tests.
409415
custom_suites: Custom test suites to add to standard tests.
410416
suite_config: SuiteConfig instance to pass to tests.
417+
streams: Streams to test.
411418
kwargs: Keyword arguments to pass to the TapRunner.
412419
413420
Returns:
@@ -423,6 +430,7 @@ def get_tap_test_class(
423430
include_tap_tests=include_tap_tests,
424431
include_stream_tests=include_stream_tests,
425432
include_stream_attribute_tests=include_stream_attribute_tests,
433+
streams=streams,
426434
**kwargs,
427435
)
428436

singer_sdk/testing/runners.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from singer_sdk.testing.config import SuiteConfig
1515

1616
if t.TYPE_CHECKING:
17+
from collections.abc import Sequence
1718
from pathlib import Path
1819

1920
from singer_sdk.helpers._compat import Traversable
@@ -96,6 +97,8 @@ def __init__(
9697
tap_class: type[Tap],
9798
config: dict | None = None,
9899
suite_config: SuiteConfig | None = None,
100+
*,
101+
streams: Sequence[str] | None = None,
99102
**kwargs: t.Any,
100103
) -> None:
101104
"""Initialize Tap instance.
@@ -105,6 +108,7 @@ def __init__(
105108
config: Config dict to pass to Tap class.
106109
suite_config (SuiteConfig): SuiteConfig instance to be used when
107110
instantiating tests.
111+
streams: Streams to test.
108112
kwargs: Default arguments to be passed to tap on create.
109113
"""
110114
super().__init__(
@@ -113,6 +117,7 @@ def __init__(
113117
suite_config=suite_config,
114118
**kwargs,
115119
)
120+
self.streams = streams
116121

117122
def new_tap(self) -> Tap:
118123
"""Get new Tap instance.
@@ -148,6 +153,7 @@ def run_sync_dry_run(self) -> bool:
148153
new_tap = self.new_tap()
149154
return new_tap.run_sync_dry_run(
150155
dry_run_record_limit=self.suite_config.max_records_limit,
156+
streams=self.streams,
151157
)
152158

153159
def sync_all(self, **kwargs: t.Any) -> None: # noqa: ARG002

tests/core/testing/test_factory.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def test_new_test_class_default_suites(
136136
tap_class=factory.tap_class,
137137
config=factory.config,
138138
suite_config=None,
139+
streams=None,
139140
parse_env_config=True,
140141
)
141142

@@ -176,6 +177,7 @@ def test_new_test_class_custom_kwargs(
176177
tap_class=factory.tap_class,
177178
config=factory.config,
178179
suite_config=suite_config,
180+
streams=None,
179181
parse_env_config=False,
180182
custom_kwarg=custom_kwarg,
181183
)
@@ -636,6 +638,7 @@ def test_get_tap_test_class(
636638
include_tap_tests=True,
637639
include_stream_tests=False,
638640
include_stream_attribute_tests=True,
641+
streams=None,
639642
custom_kwarg="value",
640643
)
641644

tests/core/testing/test_runners.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,102 @@ def test_run_sync_dry_run(self, runner: TapTestRunner):
175175
assert result is True
176176
mock_tap.run_sync_dry_run.assert_called_once_with(
177177
dry_run_record_limit=runner.suite_config.max_records_limit,
178+
streams=None,
178179
)
179180

181+
def test_init_with_streams_parameter(self, mock_tap_class):
182+
"""Test TapTestRunner initialization with streams parameter."""
183+
config = {"test": "config"}
184+
streams = ["users", "orders", "products"]
185+
186+
runner = TapTestRunner(
187+
tap_class=mock_tap_class,
188+
config=config,
189+
streams=streams,
190+
)
191+
192+
assert runner.streams == streams
193+
assert runner.singer_class is mock_tap_class
194+
assert runner.config == config
195+
196+
def test_init_with_streams_none(self, mock_tap_class):
197+
"""Test TapTestRunner initialization with streams=None."""
198+
runner = TapTestRunner(
199+
tap_class=mock_tap_class,
200+
streams=None,
201+
)
202+
203+
assert runner.streams is None
204+
205+
def test_init_with_streams_empty_sequence(self, mock_tap_class):
206+
"""Test TapTestRunner initialization with empty streams sequence."""
207+
runner = TapTestRunner(
208+
tap_class=mock_tap_class,
209+
streams=[],
210+
)
211+
212+
assert runner.streams == []
213+
214+
def test_run_sync_dry_run_with_streams(self, mock_tap_class):
215+
"""Test run_sync_dry_run method with streams parameter."""
216+
streams = ["users", "orders"]
217+
runner = TapTestRunner(
218+
tap_class=mock_tap_class,
219+
streams=streams,
220+
)
221+
222+
mock_tap = Mock(spec=Tap)
223+
mock_tap.run_sync_dry_run.return_value = True
224+
225+
with patch.object(runner, "new_tap", return_value=mock_tap):
226+
result = runner.run_sync_dry_run()
227+
228+
assert result is True
229+
mock_tap.run_sync_dry_run.assert_called_once_with(
230+
dry_run_record_limit=runner.suite_config.max_records_limit,
231+
streams=streams,
232+
)
233+
234+
def test_run_sync_dry_run_with_tuple_streams(self, mock_tap_class):
235+
"""Test run_sync_dry_run method with tuple streams parameter."""
236+
streams = ("users", "orders", "products")
237+
runner = TapTestRunner(
238+
tap_class=mock_tap_class,
239+
streams=streams,
240+
)
241+
242+
mock_tap = Mock(spec=Tap)
243+
mock_tap.run_sync_dry_run.return_value = True
244+
245+
with patch.object(runner, "new_tap", return_value=mock_tap):
246+
result = runner.run_sync_dry_run()
247+
248+
assert result is True
249+
mock_tap.run_sync_dry_run.assert_called_once_with(
250+
dry_run_record_limit=runner.suite_config.max_records_limit,
251+
streams=streams,
252+
)
253+
254+
def test_streams_parameter_with_other_kwargs(self, mock_tap_class):
255+
"""Test streams parameter works with other keyword arguments."""
256+
config = {"api_key": "test"}
257+
suite_config = SuiteConfig(max_records_limit=500)
258+
streams = ["users"]
259+
kwargs = {"parse_env_config": True}
260+
261+
runner = TapTestRunner(
262+
tap_class=mock_tap_class,
263+
config=config,
264+
suite_config=suite_config,
265+
streams=streams,
266+
**kwargs,
267+
)
268+
269+
assert runner.streams == streams
270+
assert runner.config == config
271+
assert runner.suite_config is suite_config
272+
assert runner.default_kwargs == kwargs
273+
180274
def test_clean_sync_output(self):
181275
"""Test _clean_sync_output static method."""
182276
raw_output = """{"type": "SCHEMA", "stream": "test"}
@@ -535,6 +629,53 @@ def test_tap_runner_full_workflow(self):
535629
# Verify tap was created with correct config
536630
mock_tap_class.assert_called_with(config=config)
537631

632+
def test_tap_runner_with_streams_parameter(self):
633+
"""Test a complete tap runner workflow with streams parameter."""
634+
# Create a minimal mock tap that behaves more realistically
635+
mock_tap_class = Mock(spec=Tap)
636+
mock_tap_instance = Mock(spec=Tap)
637+
mock_tap_class.return_value = mock_tap_instance
638+
639+
# Configure realistic return values
640+
mock_tap_instance.run_discovery.return_value = '{"streams": []}'
641+
mock_tap_instance.run_connection_test.return_value = True
642+
mock_tap_instance.run_sync_dry_run.return_value = True
643+
644+
config = {"api_key": "test_key"}
645+
suite_config = SuiteConfig(max_records_limit=100)
646+
streams = ["users", "orders"]
647+
648+
runner = TapTestRunner(
649+
tap_class=mock_tap_class,
650+
config=config,
651+
suite_config=suite_config,
652+
streams=streams,
653+
)
654+
655+
# Test that streams are stored correctly
656+
assert runner.streams == streams
657+
658+
# Test discovery (should not use streams)
659+
catalog = runner.run_discovery()
660+
assert catalog == '{"streams": []}'
661+
662+
# Test connection (should not use streams)
663+
connection_ok = runner.run_connection_test()
664+
assert connection_ok is True
665+
666+
# Test dry run (should pass streams parameter)
667+
dry_run_ok = runner.run_sync_dry_run()
668+
assert dry_run_ok is True
669+
670+
# Verify tap was created with correct config
671+
mock_tap_class.assert_called_with(config=config)
672+
673+
# Verify that run_sync_dry_run was called with streams parameter
674+
mock_tap_instance.run_sync_dry_run.assert_called_once_with(
675+
dry_run_record_limit=100,
676+
streams=streams,
677+
)
678+
538679
def test_target_runner_with_real_input_data(self, tmp_path: Path):
539680
"""Test target runner with realistic input data."""
540681
# Create test input file

0 commit comments

Comments
 (0)