Тестирование
(Дополнительно: Оффлайн-лекция 2021 года про квазиобъекты)
Место тестирования в жизненном цикле программного продукта
- Собирается и устанавливается — значит, работает!
- + Проверяется анализатором кода, и потыкано end-user тестерами согласно тест-плану
- + Автоматическая проверка работоспособности там, где это возможно
- + Измерение тестового покрытия (coverage)
- + Покрытие всех execution path
Классификация (слегка беспорядоччная):
- Ручное, автоматизированное, автоматическое
- unit — спецификация компонентов (функций/классов и т. п.)
integration — спецификация интерфейсов между компонентами
- system — спецификация конечного продукта
- acceptance — потребительские/рыночные/эксплуатационные/… свойства продукта
- Тест регрессий
- Производительность / безопасность / юзабилити и прочие цели
- Чёрный / прозрачный ящик
- …чёрт в ступе
Покрытие
- Функции / классы
- Строчки кода
- Различные execution path (?)
- Различные execution path с критически различными наборами данных (??)
Test-driven development:
- Альтернатива ленивому варианту «Сначала весь код, потом некоторые тесты» (когда это можно?)
- Каждая новая фича сопровождается тестом
Разработка_через_тестирование (TDD)
- сначала пишется тест и заглушка
см код падает (иначе бесполезен)
- под тест пишется код
- код не падает
код изменяется и всё равно не падает
Ошибки планирования тестов:
- green test trap: Тестирование может доказать наличие дефектов, но не их отсутствие
- red test trap: Не всякие проваленные тесты означают дефекты. Могут означать пробел в требованиях, в том числе нефункциональных
Полезные ≠ друг другу термины:
ошибка программиста при написании программы может привести к
дефекту (багу) в программе, который в свою очередь может
проявиться (или не проявиться) в виде программного сбоя
⇒ Ошибки (особенно в ДНК) исправить почти невозможно, сбои исправлять — это заметать сор под ковёр, мы работаем именно с дефектами
Стоимость исправления дефекта возрастает пропорционально его возрасту
Модульное тестирование в Python
Doctest
doctest: тест = диалог с python-интерпретатором
Пример использования doctest
Поэтому только кратко:
- Модуль
- Тестируем вручную из командной строки
- Добавляем диалог as is в docstring:
Тестирование: python3 -m doctest Moo.py
Отчёт (с успешными тестами): python3 -m doctest -v Moo.py
- Тестирование исключений
- Вообще говоря, важны только три строчки (сама команда, первая строка и сообщение с исключением), остальные можно выкинуть
Перенос тестов во внешний текстовый файл,
например, в .rst
- этот файл отлично включается в Sphinx-документацию
Запуск python3 exttest.py -v:
«Серьёзные» фреймворки
Как правило — реализация методологии xUnit
- Fixture
- Подготовка компонента к тесту: не все функции можно оттестировать сходу, иногда надо
сначала что-то создать, открыть, запустить, … (set-up)
- провести тест
удалить, закрыть, остановить, … это что-то (tear-down)
- Такое одно что-то называется fixture
- Test
Атомарная процедура тестирования. Как правило однократно сравнивает ожидаемый результат с полученным.
- Case
- Набор тестов для тестирования определённого свойства объекта. Подготавливаем окружение (fixtur-ы), изучаем заявленное свойство, в т. ч. в граничных условиях.
- SubCase
- Элемент множественного тестирования (например, при циклическом вызове теста на различных наборах данных)
- Suite
- Набор cases (на разные темы, разных больших частей, разных уровней, несовместимых с другим набором и т. п.)
- Runner
- Запускалка тестов, обработчик отчётов и т. п.
Модуль unittest
Принципы unittest:
Тестирующая функция вызывает проверочный метод assertЧтоТоТам(какие-то, параметры) или проверяет в контекстном менеджере, что выпало нужное исключение / warning
- Case — это класс, в нём несколько атомарных тестирующих функций
- Suite — это объект, в который можно добавлять Case и другие Suite
- Обычно возникает автоматически (тогда Suite — это модуль)
- Runner — это заводится автоматом, но можно задать вручную с выбором Suite
- Fixture:
В модуле: setUpModule() / tearDownModule() — один раз на Suite
В классе: .setUpClass() / .tearDownClass() — один раз на Case
В классе: .setUp…() / .tearDown…() — один раз на каждый тест
Возможности:
Примеры использования
Квазиобъекты (mock)
Основные понятия:
- Квазиобъект (mock object, mocker)
объект, создаваемый в процессе тестирования вместо «настоящего» объекта
- Как правило, умеет всё сразу (его можно вызывать с любыми параметрами, обращаться к любым полям внутри него и т. п.)
- Умеет отчитываться (такой-то метод был вызван так-то)
- Свойства объекта и его полей (которые создаются автоматически как такие же квазиобъекты) — настраиваемые. Например, можно задать возвращаемые значения, значение некоторых полей, вызываемые исключения и т. п.
- Патч (patch или monkey patch)
- подмена на время теста полей реального объекта на квазиобъекты
Нередко обладает свойством самоудаляться по окончании теста. Например, патч оформляется как контекстный менеджер
- Индикатор (sentinel)
уникальный объект, который передаётся в тестируемую подсистему, и по окончании теста должен продолжать существовать в неизменном виде и там, куда вы его положили ☺. Может выдерживать копирование и сериализацию / десериализацию.
- Если индикатор в процессе тестирования удаляется, тест не пройден
Зачем нужны квазиобъекты, статья с примером на эту тему
Небольшой пример использования квазиобъектов в В репозитории примеров к лекции
Пример чуть побольше в Модельном семестровом проекте
Ещё примеры
В модели 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
- поддержка unittest, pytest
- выборочное покрытие
- маркировка ветвлений
- подпроцессы
- журналирование контекста
Пример
В репозитории примеров к лекции
- Набор тестов
- Исключение того, что тестировать не надо
«Большие» инструментарии
Сравнение с unittest:
Обратно совместим с unittest
Test discover «из коробки» — поиск тестов по имени файла/функции/класса (есть в unittest)
Обычный assert для теста (вместо многих функций)
- Готовые фикстуры (перенаправление в/в, временные изменения классов и т. п.)
Примеры инструментарив других уровней тестирования:
Нагрузочное: LOCUST
Эксплуатационное: Robot Framework
Многоуровневое (с идейной подоплёкой): behave
- …
Вообще не тестирование, а оркестрация: tox (но на титульной странице написано, что тестирование…
- …
Д/З
- Осознать, что нуждается в unit-тестировании
- Оснастить код семестрового проекта unit-тестами (любой фреймворк)
- Зафиксировать в документации, как их запускать
- Замерить тестовое покрытие (слишком малое покрытие считается недочётом)
- Подумать над тестированием UI-приложений
Например, использовать концепцию Model-View-Controller (или её аналоги), в которой достаточно заMock-ать View
- В самом простом варианте — отделить UI от логики и тестировать логику
- Подумать над более высокими уровнями тестирования (если они нужны)
- В первую очередь — например, запуск программы целиком в режиме выполнения каких-то сценариев и сравнение результатов с эталонными
