You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .cursor/rules/pytest-best-practices.mdc
+79-3Lines changed: 79 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -25,9 +25,9 @@ alwaysApply: false
25
25
## 3. Mocking Strategy: `monkeypatch` vs. `mocker`
26
26
- **`monkeypatch` (pytest built-in):**
27
27
- **Environment & Globals:** Use for modifying global settings, environment variables (`monkeypatch.setenv()`, `monkeypatch.delenv()`), the current working directory (`monkeypatch.chdir()`), or `sys.path`.
28
-
- **Patching Attributes/Builtins:** Use `monkeypatch.setattr()` to modify attributes of classes/objects (e.g., `pathlib.Path.home`) or to replace functions/methods in external libraries or Python builtins.
28
+
- **Patching Attributes/Builtins:** Use `monkeypatch.setattr()` to modify attributes of classes/objects (e.g., `pathlib.Path.home`) or to replace functions/methods in external libraries or Python builtins. When needing to control the home directory, prefer using established project fixtures like `user_path`, `home_path`, or `config_path`. These fixtures are responsible for correctly mocking `pathlib.Path.home()` internally, typically using `monkeypatch.setattr()`. Avoid direct `monkeypatch.setattr(pathlib.Path, "home", ...)` in individual tests if a suitable project fixture exists.
29
29
- **Dictionary Items:** Use `monkeypatch.setitem()` and `monkeypatch.delitem()` for modifying dictionaries.
30
-
- Refer to [Pytest Monkeypatch Documentation](https://docs.pytest.org/en/stable/how-to/monkeypatch.html).
30
+
- Refer to [Pytest Monkeypatch Documentation](mdc:.dot-config/https:/docs.pytest.org/en/stable/how-to/monkeypatch.html).
31
31
- **`mocker` (from `pytest-mock`):**
32
32
- **Application Code:** Primarily use for patching functions, methods, or objects *within the `vcspull` application code itself* (e.g., `mocker.patch('vcspull.cli.add.some_function')`).
33
33
- **Assertions:** Use `mocker` when you need to assert how a mock was called, its return values, or to simulate side effects for your application's internal logic.
@@ -44,6 +44,76 @@ alwaysApply: false
44
44
- **Logging:** Use the `caplog` fixture to assert specific log messages when testing command output or internal logging.
45
45
- **Error Handling:** Explicitly test for expected exceptions using `pytest.raises()`.
46
46
47
+
### Parameterized Test Structure
48
+
For tests involving multiple scenarios managed by `@pytest.mark.parametrize`, use `typing.NamedTuple` to define the structure of each test case. This promotes readability and consistency.
49
+
- Include a `test_id: str` field in the `NamedTuple` for clear test identification in pytest output.
50
+
- Define a list of these `NamedTuple` instances for your test scenarios.
51
+
- Use `pytest.mark.parametrize` with `ids=lambda tc: tc.test_id` (or similar) for descriptive test names.
ids=[tc.test_id for tc in TEST_SCENARIOS] # Or ids=lambda tc: tc.test_id
73
+
)
74
+
def test_my_feature(
75
+
input_arg: str, expected_output: str, # Corresponds to NamedTuple fields (test_id usually not passed)
76
+
# ... other fixtures ...
77
+
test_id: str, # if you need test_id inside the test, otherwise omit from signature
78
+
) -> None:
79
+
# ... test logic using input_arg and asserting against expected_output ...
80
+
actual_output = f"processed_{input_arg}" # Replace with actual function call
81
+
assert actual_output == expected_output
82
+
# Note: test_id is automatically unpacked by parametrize if present in NamedTuple fields
83
+
# and also passed as an argument if included in the test function signature.
84
+
```
85
+
86
+
### Asserting CLI Output
87
+
When testing CLI commands using `capsys` (for stdout/stderr) or `caplog` (for log messages), define expected output clearly. A common pattern is to check for the presence (or absence) of a list of substring "needles" within the captured output.
88
+
89
+
```python
90
+
# Example of Asserting CLI Output
91
+
# (within a test function that uses capsys or caplog)
92
+
93
+
# Assuming your NamedTuple for parameters includes:
# output_to_check = \"\\n\".join(rec.message for rec in caplog.records)
103
+
104
+
105
+
# Generic checking logic:
106
+
# if expected_in_out is not None:
107
+
# needles = [expected_in_out] if isinstance(expected_in_out, str) else expected_in_out
108
+
# for needle in needles:
109
+
# assert needle in output_to_check, f"Expected '{needle}' in output"
110
+
111
+
# if expected_not_in_out is not None:
112
+
# needles = [expected_not_in_out] if isinstance(expected_not_in_out, str) else expected_not_in_out
113
+
# for needle in needles:
114
+
# assert needle not in output_to_check, f"Did not expect '{needle}' in output"
115
+
```
116
+
47
117
## 5. Code Coverage and Quality
48
118
- **100% Coverage:** Aim for 100% test coverage for all new or modified code in `cli/add.py` and `cli/add_from_fs.py` (and any other modules).
49
119
- **Test All Paths:** Ensure tests cover success cases, failure cases, edge conditions, and all logical branches within the code.
@@ -59,11 +129,17 @@ alwaysApply: false
59
129
## 6. `vcspull`-Specific Considerations
60
130
- **Configuration Files:**
61
131
- When testing config loading, mock `find_home_config_files` appropriately.
62
-
- Use helpers like `vcspull.tests.helpers.save_config_yaml` (which internally uses `write_config`) for creating test configuration files in a controlled manner.
132
+
- **Always** use the project's helper functions (e.g., `vcspull.tests.helpers.write_config` or the higher-level `vcspull.tests.helpers.save_config_yaml`) to create temporary `.vcspull.yaml` files within your tests (typically in `tmp_path` or `config_path`). Avoid direct `yaml.dump` and `file.write_text` for config file creation to maintain consistency and reduce boilerplate.
63
133
- **Path Expansion:** Be mindful of `expand_dir`. If testing logic that depends on its behavior, provide controlled mocks for it or ensure `home_path` / `cwd_path` fixtures correctly influence its resolution.
64
134
- **Avoid Manual VCS Subprocesses:**
65
135
- **Do not** use `subprocess.run(["git", ...])` or similar direct VCS command calls for setting up repository states in tests if a `libvcs` fixture or library function can achieve the same result.
66
136
- The only exception is when testing a function that *itself* directly uses `subprocess` (e.g., `get_git_origin_url`).
67
137
- Refactor tests like `test_add_from_fs_integration_with_libvcs` to use `create_git_remote_repo` from `libvcs` instead of manual `git init` calls via `subprocess`.
68
138
139
+
### Filesystem State Management and `shutil`
140
+
- **`tmp_path` is Primary:** Rely on `pytest` fixtures like `tmp_path` (and derived fixtures such as `user_path`, `config_path`) for creating and managing temporary files and directories needed by tests. Pytest automatically handles the cleanup of these resources.
141
+
- **Avoid Manual Cleanup of `tmp_path` Resources:** Manual cleanup of files/directories created within `tmp_path` using `shutil` (e.g., `shutil.rmtree()`) should generally be unnecessary, as pytest's fixture management handles this.
142
+
- **`shutil` for Pre-conditions:** However, `shutil` operations (or standard library functions like `os.remove`, `pathlib.Path.unlink`, `pathlib.Path.mkdir`) can be legitimately used *during the setup phase of a test (before the code under test is executed)*. This is appropriate when you need to establish specific filesystem pre-conditions *within* the `tmp_path` environment that are crucial for the test scenario.
143
+
- **Example:** Ensuring a directory does *not* exist before testing an operation that is expected to create it, or ensuring a specific file *does* exist. The usage of `if my_git_repo.is_dir(): shutil.rmtree(my_git_repo)` in `tests/test_cli.py` (within `test_sync_broken`) is an example of setting such a pre-condition: it ensures that the target directory for a repository is removed before `vcspull sync` is called, allowing tests to consistently observe cloning behavior.
144
+
69
145
By following these rules, we can ensure that new tests are robust, maintainable, consistent with the existing test suite, and effectively leverage the capabilities of `pytest` and `libvcs`.
0 commit comments