Pytest plugin for testing console scripts
pytest-console-scripts is a pytest plugin designed for comprehensive testing of Python console scripts. It offers two execution modes: an 'in-process' mode for fast development iteration by running scripts within the same interpreter as pytest, and a 'subprocess' mode to simulate real-world execution environments. The library is actively maintained, with its latest version being 1.4.1, and typically follows a minor release cadence as needed for bug fixes and Python version support.
Common errors
-
pytest.FixtureLookupError: Fixture 'script_runner' not found
cause The `pytest-console-scripts` plugin is not installed or not correctly discovered by pytest.fixEnsure `pytest-console-scripts` is installed in your test environment (`pip install pytest-console-scripts`) and that pytest is being run from a directory where it can discover the plugin. -
Your script requires user input (e.g., using `input()`) and hangs during tests, or test fails with `EOFError`.
cause The test runner provides no interactive input to the script when `input()` is called, leading to a hang or an `EOFError` when trying to read from a closed stdin.fixUse `pytest-mock` to patch `builtins.input` within your test. Example: `mocker.patch('builtins.input', return_value='my_input_string')` before running the script with `script_runner`. -
Tests are failing, but the stdout/stderr from my console script isn't showing in the pytest output.
cause By default, pytest captures stdout/stderr. Output is only shown for failing tests, or when explicitly requested.fixRun pytest with the `-s` flag (`pytest -s`) to disable output capturing for print statements and logs, allowing script output to appear directly in the console. The `script_runner.run` method also returns the `stdout` and `stderr` for programmatic inspection.
Warnings
- breaking Support for Python 3.7 has been dropped in version 1.4.1.
- breaking Support for Python 3.6 was dropped in version 1.4.0.
- gotcha When testing scripts that require user input (e.g., via `input()`), mocking might not work as expected in `subprocess` mode, as the script runs in a separate Python interpreter. Mocking is generally more reliable in `inprocess` mode.
- gotcha During development, if you add new console scripts to your `setup.py` or `pyproject.toml`, they might not be immediately discoverable by `pytest-console-scripts` without a reinstallation.
Install
-
pip install pytest-console-scripts -
pip install pytest-console-scripts==1.4.1
Imports
- script_runner
def test_my_script(script_runner): ...
Quickstart
import pytest
import subprocess
import sys
# Create a dummy console script and setup.py for demonstration
# In a real project, these would exist in your project structure
# --- my_package/setup.py ---
# from setuptools import setup, find_packages
# setup(
# name='my_package',
# version='0.1.0',
# packages=find_packages(),
# entry_points={
# 'console_scripts': [
# 'my-script=my_package.main:main'
# ]
# },
# )
# --- my_package/main.py ---
# import sys
# def main():
# if len(sys.argv) > 1 and sys.argv[1] == '--hello':
# print('Hello from my-script')
# else:
# print('Running my-script')
# Simplified setup for a runnable quickstart within one file:
# Create dummy files for demonstration purposes
# A real setup would involve 'pip install -e .' in a virtual environment.
# Create a temporary directory and files
import tempfile
import os
def create_dummy_project(tmp_path):
pkg_dir = tmp_path / "my_package"
pkg_dir.mkdir()
(pkg_dir / "__init__.py").touch()
(pkg_dir / "main.py").write_text(
"""
import sys
def main():
if len(sys.argv) > 1 and sys.argv[1] == '--hello':
print('Hello from my-script')
else:
print('Running my-script')
sys.exit(0)
"""
)
(tmp_path / "setup.py").write_text(
"""
from setuptools import setup, find_packages
setup(
name='my_package',
version='0.1.0',
packages=find_packages(),
entry_points={
'console_scripts': [
'my-script=my_package.main:main'
]
},
)
"""
)
return tmp_path
# Example Test File (e.g., test_scripts.py)
def test_my_console_script(script_runner, tmp_path):
# Set up the dummy project and install it in editable mode
project_root = create_dummy_project(tmp_path)
subprocess.run([sys.executable, '-m', 'pip', 'install', '-e', str(project_root)], check=True)
# Run the script without arguments
result = script_runner.run(['my-script'])
assert result.returncode == 0
assert 'Running my-script' in result.stdout
assert result.stderr == ''
# Run the script with an argument
result_hello = script_runner.run(['my-script', '--hello'])
assert result_hello.returncode == 0
assert 'Hello from my-script' in result_hello.stdout
assert result_hello.stderr == ''
# Test behavior in subprocess mode (optional)
result_subprocess = script_runner.run(['my-script'], launch_mode='subprocess')
assert result_subprocess.returncode == 0
assert 'Running my-script' in result_subprocess.stdout