Filesystem Isolation Examples¶
Prerequisites¶
To follow these examples, you may want to install optional mocking libraries:
# For comprehensive filesystem mocking
pip install pyfakefs
# For general mocking (usually already included with pytest)
pip install pytest-mock
These libraries are not required by pytest-test-categories but are recommended for writing hermetic tests that mock filesystem operations.
This document provides practical examples of tests that violate filesystem isolation and how to fix them.
Example 1: Writing Report Files¶
Violating Test¶
This test writes a file directly to the project directory, violating small test requirements:
# tests/test_reports.py
from pathlib import Path
import pytest
@pytest.mark.small
def test_generate_report():
"""Generate a report file."""
report_path = Path("output/report.txt")
report_path.parent.mkdir(exist_ok=True)
report_path.write_text("Test Report\n==========\nAll tests passed.")
assert report_path.exists()
assert "All tests passed" in report_path.read_text()
Error:
HermeticityViolationError: Filesystem access attempted
Attempted create on: /home/user/project/output/
Fixed Test Using pyfakefs¶
# tests/test_reports.py
import pytest
@pytest.mark.small
def test_generate_report(fs): # pyfakefs fixture
"""Generate a report file using fake filesystem."""
# Arrange: Create output directory in fake filesystem
fs.create_dir("/output")
report_path = "/output/report.txt"
# Act: Generate the report (writes to fake filesystem)
with open(report_path, "w") as f:
f.write("Test Report\n==========\nAll tests passed.")
# Assert: Verify the report was created correctly
with open(report_path) as f:
content = f.read()
assert "All tests passed" in content
Fixed Test Using tmp_path (Medium Test)¶
# tests/test_reports.py
from pathlib import Path
import pytest
@pytest.mark.medium # Medium tests can access the filesystem
def test_generate_report(tmp_path):
"""Generate a report file using pytest's tmp_path fixture."""
# Arrange: Create output directory in temp space
output_dir = tmp_path / "output"
output_dir.mkdir()
report_path = output_dir / "report.txt"
# Act: Generate the report
report_path.write_text("Test Report\n==========\nAll tests passed.")
# Assert: Verify the report was created correctly
assert report_path.exists()
assert "All tests passed" in report_path.read_text()
Fixed Test Using StringIO¶
# tests/test_reports.py
from io import StringIO
import pytest
@pytest.mark.small
def test_generate_report_content():
"""Generate report content without filesystem access."""
# Arrange: Create an in-memory buffer
buffer = StringIO()
# Act: Generate the report to the buffer
generate_report(buffer)
# Assert: Verify the report content
content = buffer.getvalue()
assert "Test Report" in content
assert "All tests passed" in content
Example 2: Reading Configuration Files¶
Violating Test¶
This test reads a real configuration file:
# tests/test_config.py
from pathlib import Path
import pytest
from myapp.config import load_config
@pytest.mark.small
def test_load_config():
"""Load configuration from file."""
config = load_config(Path("/etc/myapp/config.yaml"))
assert config["database"]["host"] == "localhost"
assert config["database"]["port"] == 5432
Error:
HermeticityViolationError: Filesystem access attempted
Attempted read on: /etc/myapp/config.yaml
Fixed Test Using Mock¶
# tests/test_config.py
import pytest
from myapp.config import load_config
CONFIG_YAML = """
database:
host: localhost
port: 5432
name: myapp_db
"""
@pytest.mark.small
def test_load_config_parses_yaml(mocker):
"""Load configuration from mocked file."""
# Arrange: Mock the file open operation
mocker.patch("builtins.open", mocker.mock_open(read_data=CONFIG_YAML))
# Act: Load the configuration
config = load_config("/etc/myapp/config.yaml")
# Assert: Verify the configuration was parsed correctly
assert config["database"]["host"] == "localhost"
assert config["database"]["port"] == 5432
Fixed Test Using tmp_path (Medium Test)¶
# tests/test_config.py
from pathlib import Path
import pytest
from myapp.config import load_config
CONFIG_YAML = """
database:
host: localhost
port: 5432
name: myapp_db
"""
@pytest.mark.medium # Medium tests can access the filesystem
def test_load_config_from_file(tmp_path):
"""Load configuration from a real file in temp space."""
# Arrange: Create config file in temp directory
config_file = tmp_path / "config.yaml"
config_file.write_text(CONFIG_YAML)
# Act: Load the configuration
config = load_config(config_file)
# Assert: Verify the configuration
assert config["database"]["host"] == "localhost"
assert config["database"]["port"] == 5432
Fixed Test Using Dependency Injection¶
# src/myapp/config.py
from io import StringIO
from pathlib import Path
from typing import TextIO
import yaml
def load_config(source: Path | TextIO) -> dict:
"""Load configuration from a file path or file-like object.
Args:
source: Either a Path to a config file, or a file-like object.
Returns:
Configuration dictionary.
"""
if isinstance(source, Path):
with open(source) as f:
return yaml.safe_load(f)
else:
return yaml.safe_load(source)
# tests/test_config.py
from io import StringIO
import pytest
from myapp.config import load_config
CONFIG_YAML = """
database:
host: localhost
port: 5432
"""
@pytest.mark.small
def test_load_config_from_stream():
"""Load configuration from a stream (no filesystem access)."""
# Arrange: Create config as StringIO
config_stream = StringIO(CONFIG_YAML)
# Act: Load from stream
config = load_config(config_stream)
# Assert: Verify configuration
assert config["database"]["host"] == "localhost"
Example 3: File Processing Pipeline¶
Violating Test¶
This test processes files from the project directory:
# tests/test_processor.py
from pathlib import Path
import pytest
from myapp.processor import process_data_files
@pytest.mark.small
def test_process_data_files():
"""Process all data files in a directory."""
data_dir = Path("data/input")
output_dir = Path("data/output")
results = process_data_files(data_dir, output_dir)
assert len(results) > 0
assert all(r.success for r in results)
Error:
HermeticityViolationError: Filesystem access attempted
Attempted list on: /home/user/project/data/input/
Fixed Test Using tmp_path (Medium Test)¶
# tests/test_processor.py
from pathlib import Path
import pytest
from myapp.processor import process_data_files
@pytest.mark.medium # Medium tests can access the filesystem
def test_process_data_files(tmp_path):
"""Process all data files in a directory."""
# Arrange: Create test directory structure
input_dir = tmp_path / "input"
output_dir = tmp_path / "output"
input_dir.mkdir()
output_dir.mkdir()
# Create test data files
(input_dir / "file1.csv").write_text("id,name\n1,Alice\n2,Bob")
(input_dir / "file2.csv").write_text("id,name\n3,Charlie")
# Act: Process the files
results = process_data_files(input_dir, output_dir)
# Assert: Verify processing results
assert len(results) == 2
assert all(r.success for r in results)
assert (output_dir / "file1_processed.csv").exists()
assert (output_dir / "file2_processed.csv").exists()
Fixed Test Using pyfakefs¶
# tests/test_processor.py
import pytest
from myapp.processor import process_data_files
@pytest.mark.small
def test_process_data_files_with_fake_fs(fs):
"""Process data files using a fake filesystem."""
# Arrange: Create fake directory structure
fs.create_dir("/data/input")
fs.create_dir("/data/output")
fs.create_file("/data/input/file1.csv", contents="id,name\n1,Alice\n2,Bob")
fs.create_file("/data/input/file2.csv", contents="id,name\n3,Charlie")
# Act: Process the files
results = process_data_files("/data/input", "/data/output")
# Assert: Verify processing
assert len(results) == 2
assert all(r.success for r in results)
Example 4: Log File Testing¶
Violating Test¶
This test checks log file creation:
# tests/test_logging.py
from pathlib import Path
import pytest
from myapp.logging import setup_logging, get_logger
@pytest.mark.small
def test_logging_creates_file():
"""Logging creates a log file."""
log_dir = Path("logs")
setup_logging(log_dir)
logger = get_logger("test")
logger.info("Test message")
log_file = log_dir / "app.log"
assert log_file.exists()
assert "Test message" in log_file.read_text()
Error:
HermeticityViolationError: Filesystem access attempted
Attempted create on: /home/user/project/logs/
Fixed Test Using tmp_path (Medium Test)¶
# tests/test_logging.py
from pathlib import Path
import pytest
from myapp.logging import setup_logging, get_logger
@pytest.mark.medium # Medium tests can access the filesystem
def test_logging_creates_file(tmp_path):
"""Logging creates a log file in temp directory."""
# Arrange: Set up logging in temp directory
log_dir = tmp_path / "logs"
log_dir.mkdir()
setup_logging(log_dir)
logger = get_logger("test")
# Act: Write a log message
logger.info("Test message")
# Assert: Verify the log file
log_file = log_dir / "app.log"
assert log_file.exists()
assert "Test message" in log_file.read_text()
Fixed Test Using StringIO Handler¶
# tests/test_logging.py
import logging
from io import StringIO
import pytest
from myapp.logging import get_logger
@pytest.mark.small
def test_logging_output():
"""Logging outputs correct messages (no file access)."""
# Arrange: Create a StringIO handler
log_stream = StringIO()
handler = logging.StreamHandler(log_stream)
handler.setFormatter(logging.Formatter("%(message)s"))
logger = get_logger("test")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Act: Write a log message
logger.info("Test message")
# Assert: Verify the log output
log_content = log_stream.getvalue()
assert "Test message" in log_content
Example 5: Database Fixture Files¶
Violating Test¶
This test loads SQL fixtures from the repository:
# tests/test_database.py
from pathlib import Path
import pytest
from myapp.database import execute_sql
@pytest.mark.small
def test_create_tables(mocker):
"""Create database tables from SQL file."""
mock_conn = mocker.Mock()
sql_file = Path("tests/fixtures/schema.sql")
execute_sql(mock_conn, sql_file.read_text())
mock_conn.execute.assert_called()
Error:
HermeticityViolationError: Filesystem access attempted
Attempted read on: /home/user/project/tests/fixtures/schema.sql
Fixed Test With Embedded SQL¶
# tests/test_database.py
import pytest
from myapp.database import execute_sql
SCHEMA_SQL = """
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(10, 2)
);
"""
@pytest.mark.small
def test_create_tables(mocker):
"""Create database tables from SQL."""
# Arrange: Create mock connection
mock_conn = mocker.Mock()
# Act: Execute the schema SQL
execute_sql(mock_conn, SCHEMA_SQL)
# Assert: Verify SQL was executed
assert mock_conn.execute.called
# Verify CREATE TABLE was in the executed SQL
executed_sql = mock_conn.execute.call_args[0][0]
assert "CREATE TABLE users" in executed_sql
Example 6: Using Medium Test Size¶
Sometimes filesystem access is genuinely required. In these cases, use a medium test:
Legitimate Filesystem Test¶
# tests/integration/test_file_io.py
from pathlib import Path
import pytest
from myapp.file_io import safe_write, atomic_replace
@pytest.mark.medium # Medium tests can access the filesystem
def test_atomic_file_replacement(tmp_path):
"""Test atomic file replacement with real filesystem."""
# Arrange: Create original file
target = tmp_path / "config.json"
target.write_text('{"version": 1}')
# Act: Atomically replace the file
atomic_replace(target, '{"version": 2}')
# Assert: File was updated atomically
assert target.read_text() == '{"version": 2}'
@pytest.mark.medium
def test_safe_write_creates_backup(tmp_path):
"""Test safe write creates backup file."""
# Arrange: Create original file
target = tmp_path / "data.txt"
target.write_text("original content")
# Act: Safe write with backup
safe_write(target, "new content", backup=True)
# Assert: Backup was created
backup = tmp_path / "data.txt.bak"
assert backup.exists()
assert backup.read_text() == "original content"
assert target.read_text() == "new content"
Configuration Examples¶
pyproject.toml¶
[tool.pytest.ini_options]
# Markers for test sizes
markers = [
"small: Fast, hermetic unit tests (< 1s)",
"medium: Integration tests with local services (< 5min)",
"large: End-to-end tests (< 15min)",
"xlarge: Extended tests (< 15min)",
]
# Enable strict filesystem and network isolation
test_categories_enforcement = "strict"
pytest.ini¶
[pytest]
markers =
small: Fast, hermetic unit tests (< 1s)
medium: Integration tests with local services (< 5min)
large: End-to-end tests (< 15min)
xlarge: Extended tests (< 15min)
test_categories_enforcement = strict
CI Pipeline Example¶
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install uv
uv sync --all-groups
- name: Run tests with filesystem isolation
run: |
uv run pytest --test-categories-enforcement=strict
Gradual Migration Example¶
# .github/workflows/test.yml
jobs:
# Warn about violations but don't fail
test-with-warnings:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests (warn mode)
run: |
uv run pytest --test-categories-enforcement=warn 2>&1 | tee test-output.txt
grep "Filesystem access violation" test-output.txt > violations.txt || true
if [ -s violations.txt ]; then
echo "::warning::Filesystem violations detected (see violations.txt)"
cat violations.txt
fi
# Strict enforcement on main branch
test-strict:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Run tests (strict mode)
run: |
uv run pytest --test-categories-enforcement=strict