Skip to content

Commit dfb5a05

Browse files
authored
Merge pull request #1 from KyleKing/claude/mdformat-mdsf-plugin-01BWCeWsaHPkRMCbeQSXLF44
2 parents 9870352 + 2e5366d commit dfb5a05

27 files changed

Lines changed: 2622 additions & 133 deletions
Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
# Common Testing Patterns Reference
2+
3+
Quick reference for pytest patterns commonly used in mdformat plugin testing.
4+
5+
## Pattern: Skipif for External Dependencies
6+
7+
Use when tests require external tools or binaries that may not be installed:
8+
9+
```python
10+
import shutil
11+
import pytest
12+
13+
BINARY_AVAILABLE = shutil.which("binary_name") is not None
14+
15+
16+
@pytest.mark.skipif(not BINARY_AVAILABLE, reason="binary not installed")
17+
def test_with_binary() -> None:
18+
"""Test behavior when binary is available."""
19+
pass
20+
21+
22+
@pytest.mark.skipif(BINARY_AVAILABLE, reason="testing fallback behavior")
23+
def test_without_binary() -> None:
24+
"""Test fallback behavior when binary is not available."""
25+
pass
26+
```
27+
28+
## Pattern: Multiple Skip Conditions
29+
30+
Combine multiple conditions for more complex skip logic:
31+
32+
```python
33+
import sys
34+
import pytest
35+
36+
37+
@pytest.mark.skipif(
38+
not BINARY_AVAILABLE or sys.platform == "win32",
39+
reason="binary not installed or on Windows",
40+
)
41+
def test_unix_with_binary() -> None:
42+
"""Test that only runs on Unix systems with binary installed."""
43+
pass
44+
```
45+
46+
## Pattern: Simple Parametrization
47+
48+
Use for tests with same logic but different single inputs:
49+
50+
````python
51+
import pytest
52+
import mdformat
53+
54+
55+
@pytest.mark.parametrize(
56+
"language",
57+
["python", "javascript", "typescript", "rust"],
58+
ids=["python", "javascript", "typescript", "rust"],
59+
)
60+
def test_multiple_languages(language: str) -> None:
61+
"""Test formatting for multiple programming languages."""
62+
unformatted = f"```{language}\ncode\n```"
63+
result = mdformat.text(unformatted, codeformatters={language})
64+
assert f"```{language}" in result
65+
````
66+
67+
## Pattern: Multiple Parameters
68+
69+
Use when tests need multiple inputs that vary together:
70+
71+
```python
72+
import pytest
73+
import mdformat
74+
75+
76+
@pytest.mark.parametrize(
77+
("text", "options", "expected"),
78+
[
79+
("input1", {"opt": True}, "output1"),
80+
("input2", {"opt": False}, "output2"),
81+
("input3", {"opt": True, "other": 5}, "output3"),
82+
],
83+
ids=["with_opt", "without_opt", "multiple_opts"],
84+
)
85+
def test_configurations(text: str, options: dict, expected: str) -> None:
86+
"""Test different configuration combinations."""
87+
result = mdformat.text(text, options=options)
88+
assert expected in result
89+
```
90+
91+
## Pattern: Complex Parametrization with Sets/Lists
92+
93+
Use when parameters are collections or need complex types:
94+
95+
````python
96+
import pytest
97+
98+
99+
@pytest.mark.parametrize(
100+
("languages", "expected_markers"),
101+
[
102+
({"python", "rust"}, ["```python", "```rust"]),
103+
({"python"}, ["```python"]),
104+
(set(), []),
105+
],
106+
ids=["multiple_languages", "single_language", "no_languages"],
107+
)
108+
def test_language_detection(
109+
languages: set[str],
110+
expected_markers: list[str],
111+
) -> None:
112+
"""Test language detection with various language sets."""
113+
result = format_text(SAMPLE_TEXT, languages=languages)
114+
for marker in expected_markers:
115+
assert marker in result
116+
````
117+
118+
## Pattern: Edge Case Consolidation
119+
120+
Consolidate multiple edge case tests into one parametrized test:
121+
122+
````python
123+
import pytest
124+
125+
126+
@pytest.mark.parametrize(
127+
("description", "code_content"),
128+
[
129+
("empty_block", ""),
130+
("whitespace_only", " \n "),
131+
("single_line", "x = 1"),
132+
("with_trailing_newline", "def hello():\n pass\n\n"),
133+
("multiple_blank_lines", "\n\n\n"),
134+
("tabs_and_spaces", "\t mixed \t"),
135+
],
136+
ids=[
137+
"empty_block",
138+
"whitespace_only",
139+
"single_line",
140+
"with_trailing_newline",
141+
"multiple_blank_lines",
142+
"tabs_and_spaces",
143+
],
144+
)
145+
def test_edge_cases(description: str, code_content: str) -> None:
146+
"""Test various edge cases in code formatting."""
147+
unformatted = f"```python\n{code_content}```"
148+
result = format_code(unformatted)
149+
assert "```python" in result
150+
````
151+
152+
## Pattern: Fixture Files with markdown-it-py
153+
154+
Load test cases from fixture files (markdown-it-py style):
155+
156+
```python
157+
from pathlib import Path
158+
import pytest
159+
from markdown_it.utils import read_fixture_file
160+
import mdformat
161+
162+
FIXTURE_PATH = Path(__file__).parent / "fixtures" / "test_cases.md"
163+
fixtures = read_fixture_file(FIXTURE_PATH)
164+
165+
166+
@pytest.mark.parametrize(
167+
("line", "title", "text", "expected"),
168+
fixtures,
169+
ids=[f[1] for f in fixtures],
170+
)
171+
def test_fixtures(line: int, title: str, text: str, expected: str) -> None:
172+
"""Test cases loaded from fixture file."""
173+
output = mdformat.text(text, extensions={"your_plugin"})
174+
assert output.rstrip() == expected.rstrip()
175+
```
176+
177+
**Fixture file format** (test_cases.md):
178+
179+
```markdown
180+
test case title
181+
.
182+
input markdown
183+
.
184+
expected output
185+
.
186+
187+
another test case
188+
.
189+
input
190+
.
191+
output
192+
.
193+
```
194+
195+
## Pattern: Idempotency Test
196+
197+
Essential test to ensure formatting is stable:
198+
199+
````python
200+
import mdformat
201+
202+
def test_idempotency() -> None:
203+
"""Test that formatting is idempotent (formatting twice gives same result)."""
204+
text = """# Example
205+
206+
```python
207+
def hello():
208+
pass
209+
````
210+
211+
""" result1 = mdformat.text(text, codeformatters={"python"}) result2 = mdformat.text(result1, codeformatters={"python"}) assert result1 == result2, "Formatting should be idempotent"
212+
213+
````
214+
215+
## Pattern: Smoke Test
216+
217+
Verify plugin loads and works with real content:
218+
219+
```python
220+
from pathlib import Path
221+
import mdformat
222+
223+
def test_plugin_loads() -> None:
224+
"""Verify that the plugin loads without errors."""
225+
pth = Path(__file__).parent / "pre-commit-test.md"
226+
content = pth.read_text()
227+
result = mdformat.text(content, extensions={"your_plugin"})
228+
pth.write_text(result) # Easier to debug with git
229+
assert result == content, "Differences found. Review in git."
230+
````
231+
232+
## Pattern: Test Helpers Module
233+
234+
Create `tests/helpers.py` for shared utilities:
235+
236+
```python
237+
"""Test helper utilities."""
238+
239+
from __future__ import annotations
240+
241+
import os
242+
import mdformat
243+
244+
_SHOW_TEXT = os.environ.get("SHOW_TEST_TEXT", "false").lower() == "true"
245+
246+
247+
def print_text(output: str, expected: str, show_whitespace: bool = False) -> None:
248+
"""Print text for debugging when SHOW_TEST_TEXT=true.
249+
250+
Usage: SHOW_TEST_TEXT=true pytest tests/test_file.py
251+
"""
252+
if _SHOW_TEXT:
253+
print("-- Output --")
254+
print(repr(output) if show_whitespace else output)
255+
print("-- Expected --")
256+
print(repr(expected) if show_whitespace else expected)
257+
print("-- <End> --")
258+
259+
260+
def format_with_plugin(
261+
text: str,
262+
*,
263+
extensions: set[str] | None = None,
264+
codeformatters: set[str] | None = None,
265+
options: dict[str, object] | None = None,
266+
) -> str:
267+
"""Format text with the plugin using consistent defaults."""
268+
return mdformat.text(
269+
text,
270+
extensions=extensions or set(),
271+
codeformatters=codeformatters or set(),
272+
options=options or {},
273+
)
274+
```
275+
276+
## Pattern: Testing Plugin Options
277+
278+
Test CLI options and configuration:
279+
280+
```python
281+
import pytest
282+
import mdformat
283+
284+
285+
@pytest.mark.parametrize(
286+
("option_value", "expected_behavior"),
287+
[
288+
(True, "formatted_with_option"),
289+
(False, "formatted_without_option"),
290+
],
291+
ids=["option_enabled", "option_disabled"],
292+
)
293+
def test_plugin_option(option_value: bool, expected_behavior: str) -> None:
294+
"""Test plugin behavior with different option values."""
295+
text = "# Test"
296+
result = mdformat.text(
297+
text,
298+
extensions={"your_plugin"},
299+
options={"your_option": option_value},
300+
)
301+
assert expected_behavior in result
302+
```
303+
304+
## Pattern: Testing with Specific Extensions
305+
306+
Test interaction with other mdformat extensions:
307+
308+
```python
309+
import pytest
310+
import mdformat
311+
312+
313+
@pytest.mark.parametrize(
314+
("extensions", "expected"),
315+
[
316+
({"your_plugin"}, "basic_output"),
317+
({"your_plugin", "gfm"}, "output_with_gfm"),
318+
({"your_plugin", "tables"}, "output_with_tables"),
319+
],
320+
ids=["standalone", "with_gfm", "with_tables"],
321+
)
322+
def test_with_extensions(extensions: set[str], expected: str) -> None:
323+
"""Test plugin interaction with other extensions."""
324+
text = "# Test"
325+
result = mdformat.text(text, extensions=extensions)
326+
assert expected in result
327+
```
328+
329+
## Pattern: Before/After Comparison
330+
331+
Show clear transformation expectations:
332+
333+
````python
334+
import pytest
335+
import mdformat
336+
337+
338+
@pytest.mark.parametrize(
339+
("input_text", "expected_output"),
340+
[
341+
# Consistent spacing
342+
("```python\ncode```", "```python\ncode\n```\n"),
343+
# Indentation normalization
344+
(" ```python\n code\n ```", "```python\ncode\n```\n"),
345+
# Language tag normalization
346+
("```PYTHON\ncode\n```", "```python\ncode\n```\n"),
347+
],
348+
ids=["add_trailing_newline", "remove_indentation", "lowercase_language"],
349+
)
350+
def test_formatting_transformations(input_text: str, expected_output: str) -> None:
351+
"""Test specific formatting transformations."""
352+
result = mdformat.text(input_text, extensions={"your_plugin"})
353+
assert result == expected_output
354+
````
355+
356+
## Best Practices Summary
357+
358+
1. **Always provide IDs**: Makes test output readable and debugging easier
359+
1. **Keep parameter names descriptive**: Use `input_text`/`expected_output` not `a`/`b`
360+
1. **Use type hints**: Required for modern Python and helps catch errors
361+
1. **Document with docstrings**: Brief explanation of what the test verifies
362+
1. **Group related tests**: Use meaningful test file organization
363+
1. **Test edge cases**: Empty strings, whitespace, special characters
364+
1. **Verify idempotency**: Essential for formatters
365+
1. **Add smoke tests**: Catch basic integration issues
366+
1. **Use helpers for repetition**: Create `tests/helpers.py` for shared code
367+
1. **Make tests independent**: Each test should be runnable in isolation
368+
369+
## Quick Command Reference
370+
371+
```bash
372+
# Run all tests
373+
tox
374+
375+
# Run specific parametrized test case
376+
tox -e test -- tests/test_file.py::test_name[test_id]
377+
378+
# Show all test IDs
379+
pytest --collect-only tests/
380+
381+
# Run with debugging output
382+
SHOW_TEST_TEXT=true tox -e test -- -vv
383+
384+
# Run tests matching pattern
385+
tox -e test -- -k "test_edge"
386+
387+
# Run last failed tests first
388+
tox -e test -- --failed-first
389+
390+
# Update snapshots if using pytest-snapshot
391+
tox -e test -- --snapshot-update
392+
```

0 commit comments

Comments
 (0)