Проверка консольных приложений: имитация ввода и анализа вывода
Методы тестирования stdin/stdout
Тестирование программ, которые читают данные из stdin и выводят результат в stdout, требует эмуляции пользовательского ввода и проверки вывода. Рассмотрим несколько подходов, от простых до сложных.
Как подменить stdin и stdout в unit-тестах с помощью pytest и monkeypatch?
Наиболее гибкий способ - использовать фикстуру monkeypatch из pytest. Она позволяет временно заменить объекты в модулях. Для подмены sys.stdin и sys.stdout создаются буферы io.StringIO.
# Программа, которая читает имя и выводит приветствие
def greet():
name = input("Введите имя: ")
print(f"Привет, {name}!")
путь к path python (путь к python (переменная path))
# Тест с monkeypatch
import io
import sys
def test_greet(monkeypatch):
input_data = "Анна"
mock_stdin = io.StringIO(input_data)
mock_stdout = io.StringIO()
monkeypatch.setattr('sys.stdin', mock_stdin)
monkeypatch.setattr('sys.stdout', mock_stdout)
greet()
output = mock_stdout.getvalue()
assert "Привет, Анна" in output
Python pathlib path (использование pathlib в python)
Типичные ошибки:
- Забыть закрыть StringIO или не сбросить позицию (seek(0)). В данном примере позиция автоматически устанавливается на начало.
- Если программа использует
sys.stdin.readline()вместоinput(), замена всё равно работает, так как оба метода читают из одного потока. - После теста monkeypatch восстанавливает оригинальные потоки автоматически.
Как протестировать целую программу через subprocess?
Если нужно проверить скрипт как отдельный процесс, используется модуль subprocess. Входные данные передаются через stdin, а вывод читается из stdout.
import subprocess
def test_script_with_subprocess():
input_data = "5\n3\n"
result = subprocess.run(
['python', 'adder.py'], # скрипт, который читает два числа и выводит сумму
input=input_data,
capture_output=True,
text=True
)
output = result.stdout
assert "8" in output
Path python 2 (путь к python 2)
Возможные сложности:
- Необходимо явно указать интерпретатор Python (может быть 'python3').
- Если программа ожидает нечисловой ввод или ввод с пробелами, надо корректно экранировать.
- Работа с большими объёмами данных может потребовать настройки буфера.
- Ошибки в программе могут быть записаны в stderr, который нужно проверять отдельно.
Как использовать unittest.mock.patch для замены stdin/stdout?
В стандартном модуле unittest есть mock.patch, который также подходит для замены атрибутов.
import unittest
from unittest.mock import patch
import io
import sys
class TestGreet(unittest.TestCase):
@patch('sys.stdin', new_callable=io.StringIO)
@patch('sys.stdout', new_callable=io.StringIO)
def test_greet(self, mock_stdout, mock_stdin):
mock_stdin.write("Мария\n")
mock_stdin.seek(0)
greet()
self.assertIn("Мария", mock_stdout.getvalue())
Python site packages path (путь к site-packages в python)
Типичные проблемы:
- Необходимо явно вызвать
seek(0)после записи данных в StringIO (как в примере). - Порядок декораторов важен: сначала патчи stdout, потом stdin, чтобы они соответствовали параметрам функции.
Как провести тестирование с помощью doctest для простых примеров?
Модуль doctest позволяет встраивать примеры ввода-вывода прямо в docstring. Это удобно для небольших функций, не требующих сложных тестовых сценариев.
def multiply(a, b):
"""
Возвращает произведение двух чисел.
>>> multiply(2, 3)
6
>>> multiply(-1, 5)
-5
"""
return a * b
установка path python (настройка переменной path для python)
Для тестирования stdin/stdout doctest может эмулировать ввод через специальную конструкцию:
def interactive_add():
"""
Считывает два числа из stdin и выводит сумму.
>>> import sys
>>> sys.stdin = io.StringIO("4\n5\n")
>>> interactive_add()
9
>>> sys.stdin = sys.__stdin__ # восстановление
"""
a = int(input())
b = int(input())
print(a + b)
Python find directory (поиск директории)
Ограничения:
- Ручное управление потоками в doctest приводит к путанице, лучше не злоупотреблять.
- Doctest подходит только для простых случаев, где ввод однозначен.
Как эмулировать stdin/stdout с помощью io.StringIO без фреймворков?
Можно напрямую сохранить оригинальные потоки и подменить, а затем восстановить вручную. Это полезно, если нет pytest или unittest.
import io
import sys
def test_greet_manual():
saved_stdin = sys.stdin
saved_stdout = sys.stdout
try:
sys.stdin = io.StringIO("Олег\n")
sys.stdout = io.StringIO()
greet()
output = sys.stdout.getvalue()
assert "Олег" in output
finally:
sys.stdin = saved_stdin
sys.stdout = saved_stdout
Python текущая директория (получение текущей рабочей директории в python)
Типичные ошибки:
- Забыть восстановить потоки в блоке finally (иначе последующие тесты сломаются).
- Многопоточность: замена небезопасна для параллельных тестов.
Как использовать временные файлы для ввода/вывода?
Если программа может читать из файла, можно записать входные данные в временный файл и перенаправить stdin на него. Но это менее удобно, чем буферы.
import tempfile
import os
import sys
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write("привет\n")
f.flush()
fname = f.name
sys.stdin = open(fname, 'r')
# ... тестирование ...
sys.stdin.close()
os.unlink(fname)
Недостатки:
- Необходимость работы с файловой системой, очистка временных файлов.
- Низкая скорость при большом количестве тестов.
Расширенные примеры тестирования stdin/stdout
Параметризованное тестирование с несколькими наборами входных данных
В pytest можно использовать декоратор @pytest.mark.parametrize для проверки множества сценариев на одной функции.
import pytest
import io
def calculate(expression):
"""Простой строковый калькулятор: читает выражение, выводит результат."""
try:
result = eval(expression)
print(result)
except Exception:
print("Ошибка")
@pytest.mark.parametrize("input_str, expected", [
("2+2\n", "4"),
("10-3\n", "7"),
("5*6\n", "30"),
("0/0\n", "Ошибка"),
])
def test_calculate(monkeypatch, input_str, expected):
monkeypatch.setattr('sys.stdin', io.StringIO(input_str))
monkeypatch.setattr('sys.stdout', io.StringIO())
calculate()
output = sys.stdout.getvalue().strip()
assert output == expected
# Результат выполнения pytest: # все 4 теста проходят успешно
Тестирование интерактивной программы с циклом (логин/пароль)
Программа может запрашивать данные несколько раз. Важно подать последовательность входных строк, включая переводы строк.
# Функция, имитирующая вход в систему
def login():
for _ in range(3):
user = input("Логин: ")
pwd = input("Пароль: ")
if user == "admin" and pwd == "secret":
print("Доступ разрешен")
return
print("Доступ запрещен")
# Тест с правильными данными с первой попытки
def test_login_success(monkeypatch):
inputs = "admin\nsecret\n"
monkeypatch.setattr('sys.stdin', io.StringIO(inputs))
monkeypatch.setattr('sys.stdout', io.StringIO())
login()
output = sys.stdout.getvalue()
assert "Доступ разрешен" in output
assert "Доступ запрещен" not in output
Тесты проходят, если логин и пароль корректны.
Проверка stderr при запуске subprocess
Если программа выводит ошибки в stderr, их можно проанализировать отдельно.
# скрипт error_printer.py
import sys
if len(sys.argv) > 1:
print("Ошибка: неверный аргумент", file=sys.stderr)
else:
print("Все хорошо")
# тест
import subprocess
result = subprocess.run(['python', 'error_printer.py', '--bad'],
capture_output=True, text=True)
assert result.returncode == 0
assert result.stderr == "Ошибка: неверный аргумент\n"
assert result.stdout == ""
stderr содержит сообщение об ошибке, stdout пуст.
Подмена sys.stdin.buffer для бинарного ввода
Некоторые программы читают бинарные данные из stdin. Для тестов можно подменить sys.stdin.buffer на io.BytesIO.
import sys
import io
def binary_reader():
data = sys.stdin.buffer.read(4)
print(len(data))
# тест
mock_stdin = io.BytesIO(b'\x00\x01\x02\x03')
sys.stdin.buffer = mock_stdin
sys.stdout = io.StringIO()
binary_reader()
output = sys.stdout.getvalue()
assert "4" in output
Вывод: 4 (длина прочитанных байт)
Тестирование с большим объёмом данных (ленивое чтение)
Если программа обрабатывает большой входной поток, важно использовать генераторы и тестировать поэтапно.
def line_counter():
count = 0
for line in sys.stdin:
count += 1
print(count)
# эмуляция 1000 строк
mock_input = "\n".join([f"line{i}" for i in range(1000)]) + "\n"
monkeypatch.setattr('sys.stdin', io.StringIO(mock_input))
monkeypatch.setattr('sys.stdout', io.StringIO())
line_counter()
output = sys.stdout.getvalue()
assert output.strip() == "1000"
Вывод: 1000
Примечание:
Во всех примерах кода предполагается, что необходимые импорты (io, sys) выполнены. Для pytest фикстура monkeypatch передаётся в тестовую функцию.