Проверка консольных приложений: имитация ввода и анализа вывода

Раздел: Основы Python -> Работа с файлами

Методы тестирования 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)

Недостатки:

  • Необходимость работы с файловой системой, очистка временных файлов.
  • Низкая скорость при большом количестве тестов.
- Python installation path (путь установки python)
- Python path exists (проверка существования пути в python)
- Python file in folder (проверка существования файла в папке)

Расширенные примеры тестирования 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 передаётся в тестовую функцию.

Тестирование через stdin/stdout в Python - comments

En
тестируется через stdin stdout python (python)