Тестирование

(Дополнительно: Оффлайн-лекция 2021 года про квазиобъекты)

Место тестирования в жизненном цикле программного продукта

Классификация (слегка беспорядоччная):

Покрытие

Test-driven development:

Ошибки планирования тестов:

Полезные ≠ друг другу термины:

⇒ Ошибки (особенно в ДНК) исправить почти невозможно, сбои исправлять — это заметать сор под ковёр, мы работаем именно с дефектами

Стоимость исправления дефекта возрастает пропорционально его возрасту

Модульное тестирование в Python

Doctest

doctest: тест = диалог с python-интерпретатором

Пример использования doctest

Поэтому только кратко:

  1. Модуль
  2. Тестируем вручную из командной строки
  3. Добавляем диалог as is в docstring:
  4. Тестирование: python3 -m doctest Moo.py

  5. Отчёт (с успешными тестами): python3 -m doctest -v Moo.py

  6. Тестирование исключений
    • Вообще говоря, важны только три строчки (сама команда, первая строка и сообщение с исключением), остальные можно выкинуть
  7. Перенос тестов во внешний текстовый файл,

    • например, в .rst

    • этот файл отлично включается в Sphinx-документацию
    • Запуск python3 exttest.py -v:

      •    1    import doctest
           2    doctest.testfile("exttest.rst")
        

«Серьёзные» фреймворки

Как правило — реализация методологии xUnit

Fixture
Подготовка компонента к тесту: не все функции можно оттестировать сходу, иногда надо
  1. сначала что-то создать, открыть, запустить, … (set-up)

  2. провести тест
  3. удалить, закрыть, остановить, … это что-то (tear-down)

    • Такое одно что-то называется fixture
Test

Атомарная процедура тестирования. Как правило однократно сравнивает ожидаемый результат с полученным.

Case
Набор тестов для тестирования определённого свойства объекта. Подготавливаем окружение (fixtur-ы), изучаем заявленное свойство, в т. ч. в граничных условиях.
SubCase
Элемент множественного тестирования (например, при циклическом вызове теста на различных наборах данных)
Suite
Набор cases (на разные темы, разных больших частей, разных уровней, несовместимых с другим набором и т. п.)
Runner
Запускалка тестов, обработчик отчётов и т. п.

Модуль unittest

Принципы unittest:

Возможности:

Примеры использования

Квазиобъекты (mock)

Основные понятия:

Квазиобъект (mock object, mocker)

объект, создаваемый в процессе тестирования вместо «настоящего» объекта

  • Как правило, умеет всё сразу (его можно вызывать с любыми параметрами, обращаться к любым полям внутри него и т. п.)
  • Умеет отчитываться (такой-то метод был вызван так-то)
  • Свойства объекта и его полей (которые создаются автоматически как такие же квазиобъекты) — настраиваемые. Например, можно задать возвращаемые значения, значение некоторых полей, вызываемые исключения и т. п.
Патч (patch или monkey patch)
подмена на время теста полей реального объекта на квазиобъекты
  • Нередко обладает свойством самоудаляться по окончании теста. Например, патч оформляется как контекстный менеджер

Индикатор (sentinel)

уникальный объект, который передаётся в тестируемую подсистему, и по окончании теста должен продолжать существовать в неизменном виде и там, куда вы его положили ☺. Может выдерживать копирование и сериализацию / десериализацию.

  • Если индикатор в процессе тестирования удаляется, тест не пройден

unittest.mock

Ещё примеры

В модели Model-View-Controller:

Приложение:

   1 class Model:
   2     def __init__(self):
   3         self.data = ""
   4 
   5 class View:
   6     def get_input(self):
   7         return input("Enter name: ")
   8 
   9     def show_output(self, name):
  10         print(f"Hello, {name}")
  11 
  12 class Controller:
  13     def __init__(self, model, view):
  14         self.model = model
  15         self.view = view
  16 
  17     def process_input(self):
  18         name = self.view.get_input()
  19         self.model.data = name
  20         self.view.show_output(self.model.data)
  21 
  22 if __name__ == "__main__":
  23     app = Controller(Model(), View())
  24     app.process_input()

Тест, подменяющий View на MagicMock

   1 import unittest
   2 from unittest.mock import MagicMock
   3 from app import Model, View, Controller
   4 
   5 class TestMVC(unittest.TestCase):
   6     def setUp(self):
   7         self.model = Model()
   8         self.mock_view = MagicMock(spec=View)
   9         self.controller = Controller(self.model, self.mock_view)
  10 
  11     def test_process_input_updates_model_and_view(self):
  12         self.mock_view.get_input.return_value = "Alice"
  13         self.controller.process_input()
  14         self.assertEqual(self.model.data, "Alice")
  15         self.mock_view.get_input.assert_called_once()
  16         self.mock_view.show_output.assert_called_with("Alice")
  17 
  18 if __name__ == "__main__":
  19     unittest.main()

В реальном tkinter:

Приложение:

   1 from tkinter import filedialog
   2 
   3 def open_file_dialog():
   4     return filedialog.askopenfilename(
   5         title="Select File",
   6         filetypes=(("Text Files", "*.txt"), ("All Files", "*.*"))
   7     )
   8 
   9 def main():
  10     file_path = open_file_dialog()
  11     print(f"Selected: {file_path}")
  12 
  13 
  14 if __name__ == "__main__":
  15     main()

Тест, подменяющий методы самого tkinter с помощью @patch

   1 import unittest
   2 from unittest.mock import patch
   3 import tkinterapp as app
   4 
   5 class TestFileDialog(unittest.TestCase):
   6 
   7     @patch('tkinter.filedialog.askopenfilename')
   8     def test_open_file_selection(self, mock_askopenfilename):
   9         """Test when a user selects a file."""
  10         mock_askopenfilename.return_value = "/mock/path/test.txt"
  11         result = app.open_file_dialog()
  12         self.assertEqual(result, "/mock/path/test.txt")
  13         mock_askopenfilename.assert_called_once()
  14 
  15     @patch('tkinter.filedialog.askopenfilename')
  16     def test_dialog_cancel(self, mock_askopenfilename):
  17         """Test when a user cancels the dialog (returns empty string)."""
  18         mock_askopenfilename.return_value = ""
  19         result = app.open_file_dialog()
  20         self.assertEqual(result, "")
  21         mock_askopenfilename.assert_called_once()

Тестовое покрытие

(тысячи их)

Модуль coverage

Пример

В репозитории примеров к лекции

«Большие» инструментарии

pytest

Статья на Хабре

Примеры инструментарив других уровней тестирования:

Тысячи их…

Д/З

LecturesCMC/PythonDevelopment2026/10_Testing (последним исправлял пользователь FrBrGeorge 2026-04-15 12:11:53)