Markdown Code Block Testing¶
Rustest can automatically discover and test Python code blocks in your markdown files, ensuring your documentation examples stay up-to-date and functional.
Overview¶
This feature is similar to pytest-codeblocks but built into rustest with better performance. It's perfect for:
- Testing documentation examples
- Ensuring README code samples work
- Validating tutorial code
- Keeping guides in sync with your codebase
Automatic Discovery¶
By default, rustest automatically discovers and tests Python code blocks in .md files:
Each Python code block is treated as a separate test case.
Markdown File Example¶
Create a markdown file (e.g., example.md):
# Example Documentation
## Basic Addition
```python
x = 1 + 1
assert x == 2
```
## String Operations
```python
text = "hello world"
assert text.startswith("hello")
assert "world" in text
```
## Using Imports
```python
from datetime import datetime
now = datetime.now()
assert isinstance(now, datetime)
```
Run rustest:
Output:
Skipping Code Blocks¶
Sometimes you want to include example code that shouldn't be executed. Use HTML comments to skip specific blocks:
The skip marker must appear **directly before** the code block (no blank lines in between).
!!! note "pytest compatibility"
For compatibility with pytest-codeblocks, `<!--pytest.mark.skip-->` and `<!--pytest-codeblocks:skip-->` also work.
## Disabling Markdown Testing
If you don't want to test markdown files, disable it with `--no-codeblocks`:
```bash
# Only test Python files, skip markdown
rustest --no-codeblocks
# Test specific directory without markdown
rustest tests/ --no-codeblocks
Language Filtering¶
Only Python code blocks are tested. Other languages are ignored:
# Documentation
```python
# This will be tested
assert 1 + 1 == 2
```
```javascript
// This is ignored
console.log("Hello");
```
```bash
# This is ignored
echo "Hello"
```
Code Block Structure¶
Simple Assertions¶
# Basic assertion
assert 2 + 2 == 4
# Multiple assertions
assert "hello".upper() == "HELLO"
assert len("test") == 4
Using Standard Library¶
from pathlib import Path
# Create a path
p = Path("/tmp/test.txt")
assert p.suffix == ".txt"
assert p.parent == Path("/tmp")
Using Your Library¶
If you're documenting a library, you can import and test it:
## Using rustest
```python
from rustest import approx
# Test floating point comparison
assert 0.1 + 0.2 == approx(0.3)
```
```python
from rustest import raises
# Test exception handling
with raises(ValueError):
int("not a number")
```
Real-World Examples¶
README Example¶
# MyLibrary
## Installation
```bash
pip install mylib
```
## Quick Start
```python
from mylib import Calculator
calc = Calculator()
result = calc.add(2, 3)
assert result == 5
```
## Advanced Usage
```python
from mylib import Calculator
calc = Calculator()
# Chained operations
result = calc.add(10, 5).multiply(2).value
assert result == 30
```
Tutorial Example¶
# Python Basics Tutorial
## Lesson 1: Variables
```python
# Create a variable
name = "Alice"
assert len(name) == 5
```
## Lesson 2: Lists
```python
# Create and manipulate lists
fruits = ["apple", "banana", "orange"]
fruits.append("grape")
assert len(fruits) == 4
assert "grape" in fruits
```
## Lesson 3: Functions
```python
# Define and test a function
def greet(name):
return f"Hello, {name}!"
result = greet("World")
assert result == "Hello, World!"
```
API Documentation Example¶
# API Reference
## User Management
Create a new user:
```python
from myapi import User
user = User(name="Alice", email="alice@example.com")
assert user.name == "Alice"
assert "@" in user.email
```
Update user details:
```python
from myapi import User
user = User(name="Bob", email="bob@example.com")
user.update_email("newemail@example.com")
assert user.email == "newemail@example.com"
```
Code Block Sharing State¶
Each Block is Isolated
Each Python code block runs in its own isolated environment. Variables from one block are NOT available in the next block.
# Example
```python
# Block 1
x = 10
```
```python
# Block 2 - FAILS! x is not defined here
assert x == 10 # NameError: name 'x' is not defined
```
If you need shared state, put it in one code block:
Handling Expected Failures¶
If you want to show code that deliberately fails, use text blocks or describe the failure:
# Error Handling
This code demonstrates an error:
```text
# This would raise an error (shown as text, not tested)
result = 1 / 0 # ZeroDivisionError
```
The correct way to handle it:
```python
from rustest import raises
with raises(ZeroDivisionError):
1 / 0
```
Best Practices¶
Keep Code Blocks Focused¶
# Less ideal - too much in one block
assert "hello".upper() == "HELLO"
assert "world".lower() == "world"
assert "Python".capitalize() == "Python"
assert "test".replace("t", "T") == "TesT"
# ... many more assertions
Use Realistic Examples¶
# Good - realistic usage
from datetime import datetime, timedelta
tomorrow = datetime.now() + timedelta(days=1)
assert tomorrow > datetime.now()
Test Important Features¶
# Good - demonstrates key functionality
from mylib import DataProcessor
processor = DataProcessor()
result = processor.analyze([1, 2, 3, 4, 5])
assert result.mean == 3.0
assert result.median == 3.0
Include Setup When Needed¶
# Good - shows complete example
from pathlib import Path
import tempfile
# Create temporary file
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write("test data")
filepath = f.name
# Verify it exists
assert Path(filepath).exists()
# Cleanup
Path(filepath).unlink()
Integration with Documentation Workflow¶
During Development¶
Test your documentation as you write it:
# Test README while editing
rustest README.md --no-capture
# Watch for changes (with external tool)
# while true; do rustest README.md; sleep 2; done
In CI/CD¶
Include markdown tests in your CI pipeline:
Pre-commit Hook¶
Test documentation before committing:
# .pre-commit-config.yaml
- repo: local
hooks:
- id: test-docs
name: Test documentation examples
entry: rustest
args: ["README.md", "docs/"]
language: system
pass_filenames: false
Programmatic Usage¶
Test markdown files from Python:
from rustest import run
# Test specific markdown file
report = run(paths=["README.md"])
print(f"Documentation tests: {report.passed} passed, {report.failed} failed")
# Test all markdown in docs/
report = run(paths=["docs/"])
# Disable markdown testing
report = run(paths=["docs/"], enable_codeblocks=False)
Limitations¶
- Each code block runs in isolation (no shared state)
- Only Python code blocks are tested
- Code blocks must be valid, complete Python code
- No support for continuation across blocks
- No support for interactive console examples (use doctest format as text if needed)
Next Steps¶
- CLI Usage - Learn about --no-codeblocks and other options
- Python API - Control markdown testing programmatically
- Writing Tests - Learn about regular Python tests