From 491aa6f300a902bffc514158d7c4f8c540e6749f Mon Sep 17 00:00:00 2001 From: AenEnlil Date: Tue, 9 Sep 2025 19:57:22 +0300 Subject: [PATCH 1/4] update test id extracting logic to extract test id from bdd tests. Slightly refactored _get_sync_title method --- pytestomatio/testing/testItem.py | 13 +++-- tests/test_testing/test_TestItem.py | 73 ++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/pytestomatio/testing/testItem.py b/pytestomatio/testing/testItem.py index 28bb51f..fcd7278 100644 --- a/pytestomatio/testing/testItem.py +++ b/pytestomatio/testing/testItem.py @@ -15,8 +15,8 @@ class TestItem: def __init__(self, item: Item): self.uid = uuid.uuid4() - self.id: str = TestItem.get_test_id(item) self.type = self._get_test_type(item.function) + self.id: str = self.get_test_id(item) self.title = self._get_pytest_title(item.name) self.sync_title = self._get_sync_test_title(item) self.resync_title = self._get_resync_test_title(item) @@ -30,7 +30,7 @@ def __init__(self, item: Item): self.source_code = inspect.getsource(item.function) self.class_name = item.cls.__name__ if item.cls else None self.artifacts = item.stash.get("artifact_urls", []) - + def to_dict(self) -> dict: result = dict() result['uid'] = str(self.uid) @@ -50,8 +50,11 @@ def to_dict(self) -> dict: def json(self) -> str: return json.dumps(self.to_dict(), indent=4) - @staticmethod - def get_test_id(item: Item) -> str | None: + def get_test_id(self, item: Item) -> str | None: + if self.type == 'bdd': + for marker in item.iter_markers(): + if marker.name.startswith('T'): + return '@' + marker.name for marker in item.iter_markers(MARKER): if marker.args: return marker.args[0] @@ -89,7 +92,7 @@ def _get_sync_test_title(self, item: Item) -> str: test_name = self._resolve_parameter_key_in_test_name(item, test_name) # Test id is present on already synced tests # New test don't have testomatio test id. - test_id = TestItem.get_test_id(item) + test_id = self.id if (test_id): test_name = f'{test_name} {test_id}' # ex. "User adds item to cart" diff --git a/tests/test_testing/test_TestItem.py b/tests/test_testing/test_TestItem.py index f5fcc99..afe5fd9 100644 --- a/tests/test_testing/test_TestItem.py +++ b/tests/test_testing/test_TestItem.py @@ -72,23 +72,81 @@ def test_init_with_class(self, mock_getsource, mock_item): assert test_item.class_name == "TestClass" - def test_get_test_id_with_marker(self, mock_item): + @patch("inspect.getsource") + def test_get_test_id_with_marker(self, mock_source, mock_item): """Test get_test_id with marker""" marker = Mock() + marker.name = 'testomatio' marker.args = ["@T87654321"] mock_item.iter_markers.return_value = iter([marker]) - - result = TestItem.get_test_id(mock_item) + test_item = TestItem(mock_item) + mock_item.iter_markers.return_value = iter([marker]) + result = test_item.get_test_id(mock_item) assert result == "@T87654321" + assert result == test_item.id + + @patch("inspect.getsource") + def test_get_test_id_with_other_markers(self, mock_source, mock_item): + """Test get_test_id with other marker""" + marker = Mock() + marker.name = 'other' + marker.args = ["@T87654321"] + markers = [marker] + mock_item.iter_markers.side_effect = lambda name=None: iter( + [m for m in markers if name is None or m.name == name]) + test_item = TestItem(mock_item) + result = test_item.get_test_id(mock_item) + + assert result is None + assert test_item.id is None - def test_get_test_id_without_marker(self, mock_item): + @patch("inspect.getsource") + def test_get_test_id_without_marker(self, mock_source, mock_item): """Test get_test_id without marker""" mock_item.iter_markers.return_value = iter([]) + test_item = TestItem(mock_item) + result = test_item.get_test_id(mock_item) - result = TestItem.get_test_id(mock_item) + assert result is None + assert test_item.id is None + @patch("inspect.getsource") + def test_get_test_id_for_bdd_test(self, mock_source, mock_item): + """Test get_test_id with correct marker for bdd test""" + marker = Mock() + marker.name = "T87654321" + mock_item.iter_markers.return_value = iter([marker]) + mock_item.function.__scenario__ = True + test_item = TestItem(mock_item) + mock_item.iter_markers.return_value = iter([marker]) + result = test_item.get_test_id(mock_item) + assert result == "@T87654321" + assert test_item.id == result + + @patch("inspect.getsource") + def test_get_test_id_for_bdd_test_with_other_marker(self, mock_source, mock_item): + """Test get_test_id without correct marker for bdd test""" + marker = Mock() + marker.name = 'other_marker' + marker.args = ["@T87654321"] + mock_item.iter_markers.return_value = iter([marker]) + mock_item.function.__scenario__ = True + test_item = TestItem(mock_item) + result = test_item.get_test_id(mock_item) assert result is None + assert test_item.id is None + + @patch("inspect.getsource") + def test_get_test_id_for_bdd_test_without_marker(self, mock_source, mock_item): + """Test get_test_id without marker for bdd test""" + mock_item.iter_markers.return_value = iter([]) + mock_item.function.__scenario__ = True + test_item = TestItem(mock_item) + result = test_item.get_test_id(mock_item) + + assert result is None + assert test_item.id is None def test_get_pytest_title_simple(self, mock_item): """Test _get_pytest_title without params""" @@ -268,9 +326,10 @@ def test_resolve_parameter_value_in_test_name(self, mock_item): assert "value1" in result assert "value_with_spaces" in result - def test_resolve_parameter_value_no_callspec(self, mock_item): + @patch("inspect.getsource") + def test_resolve_parameter_value_no_callspec(self, mock_source, mock_item): """Test _resolve_parameter_value_in_test_name without callspec""" - test_item = TestItem.__new__(TestItem) + test_item = TestItem(mock_item) mock_item.callspec = None with patch.object(test_item, '_get_test_parameter_key', return_value=["param1"]): From 32b532cc9ab262236ae69439c9ad1383147920b2 Mon Sep 17 00:00:00 2001 From: AenEnlil Date: Wed, 10 Sep 2025 14:09:33 +0300 Subject: [PATCH 2/4] fixed parametrised bdd tests reporting --- pytestomatio/testing/testItem.py | 12 +++-- tests/test_testing/test_TestItem.py | 82 ++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/pytestomatio/testing/testItem.py b/pytestomatio/testing/testItem.py index fcd7278..9cb2595 100644 --- a/pytestomatio/testing/testItem.py +++ b/pytestomatio/testing/testItem.py @@ -145,11 +145,14 @@ def _get_test_parameter_key(self, item: Item): if callspec: # callspec.params is a dict: fixture_name -> parameter_value # We only want fixture names, not the values. - param_names.update(callspec.params.keys()) - + if self.type == 'bdd': + param_names.discard('_pytest_bdd_example') + callspec_params = callspec.params.get('_pytest_bdd_example', {}) + param_names.update(callspec_params.keys()) + else: + param_names.update(callspec.params.keys()) # Return them as a list, or keep it as a set—whatever you prefer. return list(param_names) - def _resolve_parameter_key_in_test_name(self, item: Item, test_name: str) -> str: test_params = self._get_test_parameter_key(item) @@ -176,7 +179,8 @@ def _resolve_parameter_value_in_test_name(self, item: Item, test_name: str) -> s def repl(match): key = match.group(1) - value = item.callspec.params.get(key, '') + value = item.callspec.params.get(key, '') if not self.type == 'bdd' else \ + item.callspec.params.get('_pytest_bdd_example', {}).get(key, '') string_value = self._to_string_value(value) # TODO: handle "value with space" on testomatio BE https://github.com/testomatio/check-tests/issues/147 diff --git a/tests/test_testing/test_TestItem.py b/tests/test_testing/test_TestItem.py index afe5fd9..f50d24a 100644 --- a/tests/test_testing/test_TestItem.py +++ b/tests/test_testing/test_TestItem.py @@ -27,6 +27,11 @@ def mock_item(self): item.iter_markers.return_value = iter([]) return item + @pytest.fixture + def mock_bdd_item(self, mock_item): + mock_item.function.__scenario__ = True + return mock_item + @pytest.fixture def mock_item_with_marker(self, mock_item): """Mock Item with testomatio marker""" @@ -182,6 +187,17 @@ def test_get_test_parameter_key_no_params(self, mock_item): assert result == [] + @patch('inspect.getsource') + def test_get_test_parameter_key_no_params_for_bdd_test(self, mock_source, mock_bdd_item): + """Test _get_test_parameter_key without params for bdd test""" + mock_bdd_item.iter_markers.return_value = iter([]) + test_item = TestItem(mock_bdd_item) + + result = test_item._get_test_parameter_key(mock_bdd_item) + + assert test_item.type == 'bdd' + assert result == [] + def test_get_test_parameter_key_with_parametrize(self, mock_item): """Test _get_test_parameter_key with @pytest.mark.parametrize""" param_marker = Mock() @@ -195,27 +211,53 @@ def test_get_test_parameter_key_with_parametrize(self, mock_item): assert set(result) == {"param1", "param2"} - def test_get_test_parameter_key_with_callspec(self, mock_item): + @patch('inspect.getsource') + def test_get_test_parameter_key_with_callspec(self, mock_source, mock_item): """Test _get_test_parameter_key with callspec""" mock_item.iter_markers.return_value = iter([]) mock_item.callspec = Mock() mock_item.callspec.params = {"fixture1": "value1", "fixture2": "value2"} - test_item = TestItem.__new__(TestItem) + test_item = TestItem(mock_item) result = test_item._get_test_parameter_key(mock_item) assert set(result) == {"fixture1", "fixture2"} - def test_resolve_parameter_key_in_test_name(self, mock_item): + @patch('inspect.getsource') + def test_get_test_parameter_key_with_callspec_for_bdd_test(self, mock_source, mock_bdd_item): + """Test _get_test_parameter_key with callspec for bdd test""" + mock_bdd_item.iter_markers.return_value = iter([]) + mock_bdd_item.callspec = Mock() + mock_bdd_item.callspec.params = {'_pytest_bdd_example': {"fixture1": "value1", "fixture2": "value2"}} + test_item = TestItem(mock_bdd_item) + + result = test_item._get_test_parameter_key(mock_bdd_item) + + assert test_item.type == 'bdd' + assert set(result) == {"fixture1", "fixture2"} + + @patch('inspect.getsource') + def test_resolve_parameter_key_in_test_name(self, mock_source, mock_item): """Test _resolve_parameter_key_in_test_name""" - test_item = TestItem.__new__(TestItem) + test_item = TestItem(mock_item) with patch.object(test_item, '_get_test_parameter_key', return_value=["param1", "param2"]): result = test_item._resolve_parameter_key_in_test_name(mock_item, "Test name[value]") assert result == "Test name ${param1} ${param2}" + @patch('inspect.getsource') + def test_resolve_parameter_key_in_test_name_for_bdd_test(self, mock_source, mock_bdd_item): + """Test _resolve_parameter_key_in_test_name for bdd test""" + test_item = TestItem(mock_bdd_item) + + with patch.object(test_item, '_get_test_parameter_key', return_value=["param1", "param2"]): + result = test_item._resolve_parameter_key_in_test_name(mock_bdd_item, "Test name[value]") + + assert test_item.type == 'bdd' + assert result == "Test name ${param1} ${param2}" + def test_resolve_parameter_key_no_params(self, mock_item): """Test _resolve_parameter_key_in_test_name without params""" test_item = TestItem.__new__(TestItem) @@ -225,6 +267,17 @@ def test_resolve_parameter_key_no_params(self, mock_item): assert result == "Test name" + @patch('inspect.getsource') + def test_resolve_parameter_key_no_params_for_bdd_test(self, mock_source, mock_bdd_item): + """Test _resolve_parameter_key_in_test_name without params for bdd test""" + test_item = TestItem(mock_bdd_item) + + with patch.object(test_item, '_get_test_parameter_key', return_value=[]): + result = test_item._resolve_parameter_key_in_test_name(mock_bdd_item, "Test name") + + assert test_item.type == 'bdd' + assert result == "Test name" + def test_to_string_value_various_types(self, mock_item): """Test _to_string_value with different types""" test_item = TestItem.__new__(TestItem) @@ -312,9 +365,10 @@ def test_str_and_repr(self, mock_getsource, mock_item_with_marker): assert str_result == expected assert repr_result == expected - def test_resolve_parameter_value_in_test_name(self, mock_item): + @patch('inspect.getsource') + def test_resolve_parameter_value_in_test_name(self, mock_source, mock_item): """Test _resolve_parameter_value_in_test_name method""" - test_item = TestItem.__new__(TestItem) + test_item = TestItem(mock_item) mock_item.callspec = Mock() mock_item.callspec.params = {"param1": "value1", "param2": "value with spaces"} @@ -326,6 +380,22 @@ def test_resolve_parameter_value_in_test_name(self, mock_item): assert "value1" in result assert "value_with_spaces" in result + @patch('inspect.getsource') + def test_resolve_parameter_value_in_test_name_for_bdd_test(self, mock_source, mock_bdd_item): + """Test _resolve_parameter_value_in_test_name method for bdd test""" + test_item = TestItem(mock_bdd_item) + + mock_bdd_item.callspec = Mock() + mock_bdd_item.callspec.params = {'_pytest_bdd_example': {"param1": "value1", "param2": "value with spaces"}} + + with patch.object(test_item, '_get_test_parameter_key', return_value=["param1", "param2"]): + with patch.object(test_item, '_get_sync_test_title', return_value="Test ${param1} and ${param2}"): + result = test_item._resolve_parameter_value_in_test_name(mock_bdd_item, "Test name") + + assert test_item.type == 'bdd' + assert "value1" in result + assert "value_with_spaces" in result + @patch("inspect.getsource") def test_resolve_parameter_value_no_callspec(self, mock_source, mock_item): """Test _resolve_parameter_value_in_test_name without callspec""" From c0322398089fdab266c16238c6c2b0116d7a0317 Mon Sep 17 00:00:00 2001 From: AenEnlil Date: Wed, 10 Sep 2025 16:46:34 +0300 Subject: [PATCH 3/4] fixed random parameter keys order --- pytestomatio/testing/testItem.py | 22 +++++++++++----------- tests/test_testing/test_TestItem.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/pytestomatio/testing/testItem.py b/pytestomatio/testing/testItem.py index 9cb2595..5a07409 100644 --- a/pytestomatio/testing/testItem.py +++ b/pytestomatio/testing/testItem.py @@ -123,9 +123,9 @@ def _get_resync_test_title(self, name: str) -> str: else: return name - def _get_test_parameter_key(self, item: Item): + def _get_test_parameter_key(self, item: Item) -> list: """Return a list of parameter names for a given test item.""" - param_names = set() + param_names = [] # 1) Look for @pytest.mark.parametrize for mark in item.iter_markers('parametrize'): @@ -135,9 +135,9 @@ def _get_test_parameter_key(self, item: Item): arg_string = mark.args[0] # If the string has commas, split it into multiple names if ',' in arg_string: - param_names.update(name.strip() for name in arg_string.split(',')) + param_names.extend([name.strip() for name in arg_string.split(',') if name not in param_names]) else: - param_names.add(arg_string.strip()) + param_names.append(arg_string.strip()) # 2) Look for fixture parameterization (including dynamically generated) # via callspec, which holds *all* final parameters for an item. @@ -145,14 +145,14 @@ def _get_test_parameter_key(self, item: Item): if callspec: # callspec.params is a dict: fixture_name -> parameter_value # We only want fixture names, not the values. + callspec_params = callspec.params if self.type == 'bdd': - param_names.discard('_pytest_bdd_example') - callspec_params = callspec.params.get('_pytest_bdd_example', {}) - param_names.update(callspec_params.keys()) - else: - param_names.update(callspec.params.keys()) - # Return them as a list, or keep it as a set—whatever you prefer. - return list(param_names) + bdd_fixture_wrapper_name = '_pytest_bdd_example' + if bdd_fixture_wrapper_name in param_names: + param_names.remove(bdd_fixture_wrapper_name) + callspec_params = callspec.params.get(bdd_fixture_wrapper_name, {}) + param_names.extend([name for name in callspec_params.keys() if name not in param_names]) + return param_names def _resolve_parameter_key_in_test_name(self, item: Item, test_name: str) -> str: test_params = self._get_test_parameter_key(item) diff --git a/tests/test_testing/test_TestItem.py b/tests/test_testing/test_TestItem.py index f50d24a..5bb25c0 100644 --- a/tests/test_testing/test_TestItem.py +++ b/tests/test_testing/test_TestItem.py @@ -224,6 +224,21 @@ def test_get_test_parameter_key_with_callspec(self, mock_source, mock_item): assert set(result) == {"fixture1", "fixture2"} + @patch('inspect.getsource') + def test_get_test_parameter_key_keeps_key_order(self, mock_source, mock_item): + """Test _get_test_parameter_key keeps key order""" + mock_item.iter_markers.return_value = iter([]) + mock_item.callspec = Mock() + mock_item.callspec.params = {"fixture1": "value1", "fixture2": "value2"} + + test_item = TestItem(mock_item) + + result = test_item._get_test_parameter_key(mock_item) + + assert set(result) == {"fixture1", "fixture2"} + assert result[0] == 'fixture1' + assert result[1] == 'fixture2' + @patch('inspect.getsource') def test_get_test_parameter_key_with_callspec_for_bdd_test(self, mock_source, mock_bdd_item): """Test _get_test_parameter_key with callspec for bdd test""" From 82019951603593b05e391b71bfeb761aaf19a43f Mon Sep 17 00:00:00 2001 From: AenEnlil Date: Wed, 10 Sep 2025 17:26:17 +0300 Subject: [PATCH 4/4] bump: version 2.10.1b4 -> 2.10.1b5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 726c06e..b47c4c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ version_provider = "pep621" update_changelog_on_bump = false [project] name = "pytestomatio" -version = "2.10.1b4" +version = "2.10.1b5" dependencies = [ "requests>=2.29.0",