Различия между версиями 1 и 3 (по 2 версиям)
Версия 1 от 2020-04-21 16:05:23
Размер: 6015
Редактор: FrBrGeorge
Комментарий:
Версия 3 от 2020-04-21 21:44:03
Размер: 15371
Редактор: FrBrGeorge
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 33: Строка 33:

=== Doctest ===
Строка 41: Строка 43:
 {{{#!python
george@inspiron:~/src/moo> python3 -i Moo.py
 {{{#!highlight pycon
$ python3 -i Moo.py
Строка 81: Строка 83:
 {{{#!sh
george@inspiron:~/src/moo> python3 -m doctest Moo.py
 {{{#!highlight console
$
python3 -m doctest Moo.py
Строка 97: Строка 99:
 {{{#!sh
george@inspiron:~/src/moo> python3 -m doctest -v Moo.py
 {{{#!highlight console
$
python3 -m doctest -v Moo.py
Строка 140: Строка 142:
'''TODO'''
 * unittest
 * py.test , nose
 * '''[[https://tox.readthedocs.io/en/latest/|tox]]'''
 * Показатель покрытия кода тестами (coverage)

'''TODO''' pytest/cov

Тестирование исключений:
{{{#!highlight pycon
$ python -im Moo
>>> moo("QQ")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/george/src/tests/Moo.py", line 33, in moo
    return "M"+"o"*moos+end
TypeError: can't multiply sequence by non-int of type 'str'
>>>
}}}

Как обычно, добавим ''просто весь вывод''!
{{{#!python
def moo(oos=2, end=""):
    '''Издать мычание длиной oos с end в конце
...

Здесь должно быть исключение:
>>> moo("QQ")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/george/src/tests/Moo.py", line 33, in moo
    return "M"+"o"*moos+end
TypeError: can't multiply sequence by non-int of type 'str'
'''
}}}

Вообще говоря, важны только три строчки, остальные можно выкинуть
{{{
...
Здесь должно быть исключение:
>>> moo("QQ")
Traceback (most recent call last):
TypeError: can't multiply sequence by non-int of type 'str'
...
}}}

Тесты должны пройти!

[[py3doc:/doctest.html#simple-usage-checking-examples-in-a-text-file|Перенос тестов во внешний файл]]
 
Пишем файл ( <!> можно в `.rst`, для Sphinx), например, `exttest.rst`:
{{{#!highlight rst
External test
=============

Using Moo
---------

Start from importing `Moo` module:

        >>> import Moo

Then call `moo`:
        >>> Moo.moo(5)
        'Mooooo'
}}}

К нему запускалку тестов:
{{{#!highlight python
import doctest
doctest.testfile("exttest.rst")
}}}

И запускаем её (ключи как у модуля `pytest`):
{{{#!console
$ 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:: Подготовка компонента к тесту: не все функции можно оттестировать сходу, иногда надо
  1. сначала что-то создать, открыть, запустить, … (''set-up'')
  1. провести тест
  1. удалить, закрыть, остановить, … это что-то (''tear-down'')
  Такое ''одно'' что-то называет fixture
 Case:: ''Что именно'' тестируем. Подготавливаем окружение (fixtur-ы), что-то дёргаем, смотрим, подходит ли результат
 Suite:: Набор cases (на разные темы, разных больших частей, разных уровней и т. п.)
 Runner:: Запускалка тестов, обработчик отчётов и т. п.

[[py3doc:unittest]]
 * [[https://tirinox.ru/unit-test-python/|методичка]]

[[https://docs.pytest.org|pytest]]
 * Сравнение с `unittest`:
  * Test discover «из коробки» — поиск тестов по имени файла/функции/класаа (есть в `unittest`)
  * Обычный `assert` для теста (вместо многих функций)
  * Атомарные фикстуры (пеернаправление в/в, временные изменения классов и т. п.)
  * Множество дополнений

Ещё тестеры:
 * [[https://tox.readthedocs.io/en/latest/|tox]]
 * [[https://nox.thea.codes/en/stable/|nox]]
 * …

==== Пример: pudb ====
[[https://documen.tician.de/pudb/|PuDB]] — отладчик для питона

Соберём под него окружение:
{{{#!highlight console
[george@inspiron src]$ python3 -m venv init demo-pudb
[george@inspiron src]$ cd demo-pudb
[george@inspiron demo-pudb]$ . ./bin/activate
(demo-pudb) [george@inspiron demo-pudb]$ git clone https://github.com/inducer/pudb.git
Cloning into 'pudb'...
cd pudb
(demo-pudb) [george@inspiron pudb]$ ls
debug_me.py example-theme.py pudb setup.py
doc LICENSE README.rst test
example-shell.py MANIFEST.in requirements.dev.txt try-the-debugger.sh
example-stringifier.py manual-tests setup.cfg upload_coverage.sh
(demo-pudb) [george@inspiron pudb]$
}}}

Cоберём модуль:
{{{#!highlight console
(demo-pudb) [george@inspiron pudb]$ pip install -r requirements.dev.txt
Collecting codecov==2.0.5 (from -r requirements.dev.txt (line 1))
. . .
Successfully installed Pygments-2.2.0 argparse-1.4.0 . . .
(demo-pudb) [george@inspiron pudb]$ python setup.py build
running build
. . .
copying pudb/ui_tools.py -> build/lib/pudb
(demo-pudb) [george@inspiron pudb]$
}}}
 * Все нужные зависимости указаны в файла `requirements.dev.txt`
 * Собранный модуль в чистом виде лежит в `build/lib/pudb`
 * Для запуска тестов надо сделать так, чтобы `build/lib` попал в `PYTHONPATH`, тогда модуль будет экспортирован оттуда

{{{#!highlight console
(demo-pudb) [george@inspiron pudb]$ export PYTHONPATH=`pwd`/build/lib
(demo-pudb) [george@inspiron pudb]$ pytest
========================= test session starts =========================
platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: /home/george/src/demo-pudb/pudb, inifile:
plugins: mock-1.10.0, cov-2.4.0
collected 16 items

test/test_lowlevel.py ....
test/test_make_canvas.py .....
test/test_settings.py ..
test/test_source_code_providers.py ....
test/test_var_view.py .

====================== 16 passed in 0.22 seconds ======================
}}}
 * `pytest` сам нашёл, где лежат тесты
 * Можно сказать `pytest -v` для отчёта по каждому тесту в файле

В этом проекте используется два дополнения к `pytest`:
 * вместо полноценных фикстур — т. н. моккеры (mock):
 {{{#!highlight console
(demo-pudb) [george@inspiron pudb]$ pytest --fixtures-per-test
=========================== test session starts ============================
platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: /home/george/src/demo-pudb/pudb, inifile:
plugins: mock-1.10.0, cov-2.4.0
collected 16 items

------------------ fixtures used by test_load_breakpoints ------------------
------------------------ (test/test_settings.py:10) ------------------------
mocker
    return an object that has the same interface to the `mock` module, but
    takes care of automatically undoing all patches after each test method.
pytestconfig
    the pytest config object with access to command line opts.
. . .
}}}
 * Замер ''покрытия'' кода тестами `cov`
 {{{#!highlight console
(demo-pudb) [george@inspiron pudb]$ pytest --cov=build/lib/pudb
=========================== test session starts =======================
platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: /home/george/src/demo-pudb/pudb, inifile:
plugins: mock-1.10.0, cov-2.4.0
collected 16 items

test/test_lowlevel.py ....
test/test_make_canvas.py .....
test/test_settings.py ..
test/test_source_code_providers.py ....
test/test_var_view.py .

----------- coverage: platform linux, python 3.8.2-final-0 -----------
Name Stmts Miss Branch BrPart Cover
-----------------------------------------------------------------
build/lib/pudb/__init__.py 194 159 26 4 18%
build/lib/pudb/__main__.py 3 3 0 0 0%
build/lib/pudb/b.py 14 14 2 0 0%
build/lib/pudb/debugger.py 1386 1276 223 2 7%
build/lib/pudb/ipython.py 31 31 8 0 0%
build/lib/pudb/lowlevel.py 134 61 56 7 51%
build/lib/pudb/py3compat.py 23 10 2 1 56%
build/lib/pudb/remote.py 120 120 12 0 0%
build/lib/pudb/run.py 27 27 0 0 0%
build/lib/pudb/settings.py 378 302 88 13 22%
build/lib/pudb/shell.py 137 137 12 0 0%
build/lib/pudb/source_view.py 230 167 36 7 26%
build/lib/pudb/theme.py 595 595 2 0 0%
build/lib/pudb/ui_tools.py 222 159 66 0 25%
build/lib/pudb/var_view.py 396 324 92 6 18%
-----------------------------------------------------------------
TOTAL 3890 3385 625 40 13%
======================== 16 passed in 0.55 seconds ===================
}}}
 * Без указания `--cov=build/lib/pudb` ключ `--cov` посчитает покрытие ''всего'' запускаемого кода на python (включая все системные библиотеки:)
 * Вместо дополнения к `pytest` можно использовать отдельный модуль [[https://coverage.readthedocs.io|coverage]]

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

много теории без питона тут

  • Место тестирования в жизненном цикле программного продукта
    • Собирается — значит, работает!
    • + Проверяется анализатором кода, и потыкано end-user тестерами согласно тест-плану
    • + Автоматическая проверка работоспособности там, где это возможно
    • + Измерение тестового покрытия (coverage)
  • Ручное, автоматизированное, автоматическое
  • Виды тестирования

    • Уровни

      • unit — спецификация компонентов (функций/классов и т. п.)
      • integration — спецификация интерфейсов между компонентами

      • system — спецификация конечного продукта
      • acceptacne — потребительские/рыночные/эксплутационные/… свойства продукта
  • Дисциплина:
    • Сначала весь код, потом некоторые тесты (когда это можно?)
    • Каждая новая фича сопровождается тестом
    • Разработка_через_тестирование (TDD)

      1. сначала пишется тест и заглушка
      2. сам код падает (иначе бесполезен)

      3. под тест пишется код
      4. код не падает
      5. код изменяется и всё равно не падает

  • green test trap: Тестирование может доказать наличие дефектов, но не их отсутствие
  • red test trap: Не всякие проваленные тесты означают дефекты. Могут означать пробел в требованиях, в том числе нефункциональных
  • Полезные ≠ друг другу термины:
    • ошибка программиста при написании программы может привести к

    • дефекту (багу) в программе, который в свою очередь может

    • проявиться (или не проявиться) в виде программного сбоя

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

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

Doctest

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

  1. Модуль
       1 def moo(oos=2, end=""):
       2     '''Издать мычание длиной oos с end в конце'''
       3     return "M"+"o"*oos+end
    
  2. Тестируем вручную:
       1 $ python3 -i Moo.py
       2 >>> moo()
       3 'Moo'
       4 >>> moo(4)
       5 'Moooo'
       6 >>> moo(0)
       7 'M'
       8 >>> moo(end='!')
       9 'Moo!'
      10 >>> moo(0,'?')
      11 'M?'
      12 
    
  3. Добавляем тесты в 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
  4. Тестирование:
       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 
    
  5. Отчёт (с успешными тестами):
       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 $ python -im Moo
   2 >>> moo("QQ")
   3 Traceback (most recent call last):
   4   File "<stdin>", line 1, in <module>
   5   File "/home/george/src/tests/Moo.py", line 33, in moo
   6     return "M"+"o"*moos+end
   7 TypeError: can't multiply sequence by non-int of type 'str'
   8 >>>
   9 

Как обычно, добавим просто весь вывод!

   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:

   1 External test
   2 =============
   3 
   4 Using Moo
   5 ---------
   6 
   7 Start from importing `Moo` module:
   8 
   9         >>> import Moo
  10 
  11 Then call `moo`:
  12         >>> Moo.moo(5)
  13         'Mooooo'

К нему запускалку тестов:

   1 import doctest
   2 doctest.testfile("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
Подготовка компонента к тесту: не все функции можно оттестировать сходу, иногда надо
  1. сначала что-то создать, открыть, запустить, … (set-up)

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

Case

Что именно тестируем. Подготавливаем окружение (fixtur-ы), что-то дёргаем, смотрим, подходит ли результат

Suite
Набор cases (на разные темы, разных больших частей, разных уровней и т. п.)
Runner
Запускалка тестов, обработчик отчётов и т. п.

unittest

pytest

  • Сравнение с 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

LecturesCMC/PythonDevelopment2020/09_Testing (последним исправлял пользователь FrBrGeorge 2020-04-21 21:44:03)