Тестирование
- Место тестирования в жизненном цикле программного продукта
- Собирается — значит, работает!
- + Проверяется анализатором кода, и потыкано end-user тестерами согласно тест-плану
- + Автоматическая проверка работоспособности там, где это возможно
- + Измерение тестового покрытия (coverage)
- Ручное, автоматизированное, автоматическое
- unit — спецификация компонентов (функций/классов и т. п.)
integration — спецификация интерфейсов между компонентами
- system — спецификация конечного продукта
- acceptacne — потребительские/рыночные/эксплутационные/… свойства продукта
- Дисциплина:
- Сначала весь код, потом некоторые тесты (когда это можно?)
- Каждая новая фича сопровождается тестом
Разработка_через_тестирование (TDD)
- сначала пишется тест и заглушка
сам код падает (иначе бесполезен)
- под тест пишется код
- код не падает
код изменяется и всё равно не падает
- green test trap: Тестирование может доказать наличие дефектов, но не их отсутствие
- red test trap: Не всякие проваленные тесты означают дефекты. Могут означать пробел в требованиях, в том числе нефункциональных
- Полезные ≠ друг другу термины:
ошибка программиста при написании программы может привести к
дефекту (багу) в программе, который в свою очередь может
проявиться (или не проявиться) в виде программного сбоя
- Стоимость исправления дефекта возрастает пропорционально его «возрасту»
- Непрерывная интеграция
Модульное тестирование в Python
Doctest
doctest: тест = диалог с python-интерпретатором
- Модуль
- Тестируем вручную:
- Добавляем тесты в docstring:
def moo(oos=2, end=""): '''Издать мычание длиной oos с end в конце Оба параметра необязательны: >>> moo() 'Moo' Первый задаёт количество букв 'o' в слове 'Moo' >>> moo(4) 'Mooooo' Букв 'o' может и не быть >>> moo(0) 'M' Второй задаёт символ после всех 'o' (по умолчанию — ничего) >>> moo(end='!') 'Moo!' >>> moo(0,'?') 'M?' ''' return "M"+"o"*oos+end
- Тестирование:
1 $ python3 -m doctest Moo.py 2 ********************************************************************** 3 File "/home/george/src/moo/Moo.py", line 13, in Moo.moo 4 Failed example: 5 moo(4) 6 Expected: 7 'Mooooo' 8 Got: 9 'Moooo' 10 ********************************************************************** 11 1 items had failures: 12 1 of 5 in Moo.moo 13 ***Test Failed*** 1 failures. 14
- Отчёт (с успешными тестами):
1 $ python3 -m doctest -v Moo.py 2 Trying: 3 moo() 4 Expecting: 5 'Moo' 6 ok 7 Trying: 8 moo(4) 9 Expecting: 10 'Mooooo' 11 ********************************************************************** 12 File "/home/george/src/moo/Moo.py", line 13, in Moo.moo 13 Failed example: 14 moo(4) 15 Expected: 16 'Mooooo' 17 Got: 18 'Moooo' 19 Trying: 20 moo(0) 21 Expecting: 22 'M' 23 ok 24 Trying: 25 moo(end='!') 26 Expecting: 27 'Moo!' 28 ok 29 Trying: 30 moo(0,'?') 31 Expecting: 32 'M?' 33 ok 34 1 items had no tests: 35 Moo 36 ********************************************************************** 37 1 items had failures: 38 1 of 5 in Moo.moo 39 5 tests in 2 items. 40 4 passed and 1 failed. 41 ***Test Failed*** 1 failures. 42
Тестирование исключений:
Как обычно, добавим просто весь вывод!
1 def moo(oos=2, end=""):
2 '''Издать мычание длиной oos с end в конце
3 ...
4
5 Здесь должно быть исключение:
6 >>> moo("QQ")
7 Traceback (most recent call last):
8 File "<stdin>", line 1, in <module>
9 File "/home/george/src/tests/Moo.py", line 33, in moo
10 return "M"+"o"*moos+end
11 TypeError: can't multiply sequence by non-int of type 'str'
12 '''
Вообще говоря, важны только три строчки, остальные можно выкинуть
... Здесь должно быть исключение: >>> moo("QQ") Traceback (most recent call last): TypeError: can't multiply sequence by non-int of type 'str' ...
Тесты должны пройти!
Перенос тестов во внешний файл
Пишем файл ( можно в .rst, для Sphinx), например, exttest.rst:
К нему запускалку тестов:
И запускаем её (ключи как у модуля pytest):
$ python3 exttest.py -v Trying: import Moo Expecting nothing ok Trying: Moo.moo(5) Expecting: 'Mooooo' ok 1 items passed all tests: 2 tests in exttest.rst 2 tests in 1 items. 2 passed and 0 failed. Test passed.
«Серьёзные» фреймворки
- Fixture
- Подготовка компонента к тесту: не все функции можно оттестировать сходу, иногда надо
сначала что-то создать, открыть, запустить, … (set-up)
- провести тест
удалить, закрыть, остановить, … это что-то (tear-down) Такое одно что-то называет fixture
- Case
Что именно тестируем. Подготавливаем окружение (fixtur-ы), что-то дёргаем, смотрим, подходит ли результат
- Suite
- Набор cases (на разные темы, разных больших частей, разных уровней и т. п.)
- Runner
- Запускалка тестов, обработчик отчётов и т. п.
Сравнение с unittest:
Test discover «из коробки» — поиск тестов по имени файла/функции/класаа (есть в unittest)
Обычный assert для теста (вместо многих функций)
- Атомарные фикстуры (пеернаправление в/в, временные изменения классов и т. п.)
- Множество дополнений
Ещё тестеры:
Пример: pudb
PuDB — отладчик для питона
Соберём под него окружение:
1 [george@inspiron src]$ python3 -m venv init demo-pudb
2 [george@inspiron src]$ cd demo-pudb
3 [george@inspiron demo-pudb]$ . ./bin/activate
4 (demo-pudb) [george@inspiron demo-pudb]$ git clone https://github.com/inducer/pudb.git
5 Cloning into 'pudb'...
6 cd pudb
7 (demo-pudb) [george@inspiron pudb]$ ls
8 debug_me.py example-theme.py pudb setup.py
9 doc LICENSE README.rst test
10 example-shell.py MANIFEST.in requirements.dev.txt try-the-debugger.sh
11 example-stringifier.py manual-tests setup.cfg upload_coverage.sh
12 (demo-pudb) [george@inspiron pudb]$
13
Cоберём модуль:
1 (demo-pudb) [george@inspiron pudb]$ pip install -r requirements.dev.txt
2 Collecting codecov==2.0.5 (from -r requirements.dev.txt (line 1))
3 . . .
4 Successfully installed Pygments-2.2.0 argparse-1.4.0 . . .
5 (demo-pudb) [george@inspiron pudb]$ python setup.py build
6 running build
7 . . .
8 copying pudb/ui_tools.py -> build/lib/pudb
9 (demo-pudb) [george@inspiron pudb]$
10
Все нужные зависимости указаны в файла requirements.dev.txt
Собранный модуль в чистом виде лежит в build/lib/pudb
Для запуска тестов надо сделать так, чтобы build/lib попал в PYTHONPATH, тогда модуль будет экспортирован оттуда
1 (demo-pudb) [george@inspiron pudb]$ export PYTHONPATH=`pwd`/build/lib
2 (demo-pudb) [george@inspiron pudb]$ pytest
3 ========================= test session starts =========================
4 platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
5 rootdir: /home/george/src/demo-pudb/pudb, inifile:
6 plugins: mock-1.10.0, cov-2.4.0
7 collected 16 items
8
9 test/test_lowlevel.py ....
10 test/test_make_canvas.py .....
11 test/test_settings.py ..
12 test/test_source_code_providers.py ....
13 test/test_var_view.py .
14
15 ====================== 16 passed in 0.22 seconds ======================
16
pytest сам нашёл, где лежат тесты
Можно сказать pytest -v для отчёта по каждому тесту в файле
В этом проекте используется два дополнения к pytest:
- вместо полноценных фикстур — т. н. моккеры (mock):
1 (demo-pudb) [george@inspiron pudb]$ pytest --fixtures-per-test 2 =========================== test session starts ============================ 3 platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 4 rootdir: /home/george/src/demo-pudb/pudb, inifile: 5 plugins: mock-1.10.0, cov-2.4.0 6 collected 16 items 7 8 ------------------ fixtures used by test_load_breakpoints ------------------ 9 ------------------------ (test/test_settings.py:10) ------------------------ 10 mocker 11 return an object that has the same interface to the `mock` module, but 12 takes care of automatically undoing all patches after each test method. 13 pytestconfig 14 the pytest config object with access to command line opts. 15 . . . 16
Замер покрытия кода тестами cov
1 (demo-pudb) [george@inspiron pudb]$ pytest --cov=build/lib/pudb 2 =========================== test session starts ======================= 3 platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 4 rootdir: /home/george/src/demo-pudb/pudb, inifile: 5 plugins: mock-1.10.0, cov-2.4.0 6 collected 16 items 7 8 test/test_lowlevel.py .... 9 test/test_make_canvas.py ..... 10 test/test_settings.py .. 11 test/test_source_code_providers.py .... 12 test/test_var_view.py . 13 14 ----------- coverage: platform linux, python 3.8.2-final-0 ----------- 15 Name Stmts Miss Branch BrPart Cover 16 ----------------------------------------------------------------- 17 build/lib/pudb/__init__.py 194 159 26 4 18% 18 build/lib/pudb/__main__.py 3 3 0 0 0% 19 build/lib/pudb/b.py 14 14 2 0 0% 20 build/lib/pudb/debugger.py 1386 1276 223 2 7% 21 build/lib/pudb/ipython.py 31 31 8 0 0% 22 build/lib/pudb/lowlevel.py 134 61 56 7 51% 23 build/lib/pudb/py3compat.py 23 10 2 1 56% 24 build/lib/pudb/remote.py 120 120 12 0 0% 25 build/lib/pudb/run.py 27 27 0 0 0% 26 build/lib/pudb/settings.py 378 302 88 13 22% 27 build/lib/pudb/shell.py 137 137 12 0 0% 28 build/lib/pudb/source_view.py 230 167 36 7 26% 29 build/lib/pudb/theme.py 595 595 2 0 0% 30 build/lib/pudb/ui_tools.py 222 159 66 0 25% 31 build/lib/pudb/var_view.py 396 324 92 6 18% 32 ----------------------------------------------------------------- 33 TOTAL 3890 3385 625 40 13% 34 ======================== 16 passed in 0.55 seconds =================== 35
Без указания --cov=build/lib/pudb ключ --cov посчитает покрытие всего запускаемого кода на python (включая все системные библиотеки:)
Вместо дополнения к pytest можно использовать отдельный модуль coverage
Д/З
- Осознать, что нуждается в unit-тестировании
- Оснастить код семестрового проекта unit-тестами (любой фреймворк)
- Зафиксировать в документации, как их запускать
- Подумать над тестированием UI
например, с помощью порождения событий tkinter