Spec-Zone.ru › NumPy 1.19

Руководство по тестированию

Введение

До релиза 1.15 NumPy использовал фреймворк для тестирования nose, теперь используется фреймворк pytest. Старый фреймворк всё ещё поддерживается для поддержки проектов, использующих старый фреймворк numpy, но все тесты для NumPy должны использовать pytest.

Наша цель — обеспечить тщательный набор юнит-тестов для каждого модуля и пакета в SciPy и NumPy. Эти тесты должны проверять полную функциональность данной процедуры, а также её устойчивость к ошибочным или неожиданным входным аргументам. Многолетний опыт показал, что лучше всего писать тесты до написания или изменения кода — это разработка, управляемая тестированием. Аргументы в пользу этого могут показаться довольно абстрактными, но мы уверяем вас, что вы убедитесь, что написание тестов в первую очередь приводит к более надёжному и лучшему коду. Хорошо спроектированные тесты с хорошим покрытием существенно улучшают лёгкость рефакторинга. В случае обнаружения новой ошибки в процедуре, вы должны написать новый тест для этого конкретного случая и добавить его в набор тестов, чтобы предотвратить повторное появление этой ошибки незамеченной.

Для запуска полного набора тестов SciPy используйте следующее:

>>> import scipy
>>> scipy.test()

или из командной строки:

$ python runtests.py

SciPy использует фреймворк для тестирования из numpy.testing, поэтому все примеры SciPy, показанные здесь, также применимы к NumPy. Полный набор тестов NumPy можно запустить следующим образом:

>>> import numpy
>>> numpy.test()

Метод тестирования может принимать два или более аргументов; первый, label — это строка, указывающая, что должно быть проверено, а второй, verbose — целое число, задающее уровень детализации вывода. Подробности см. в документации к numpy.test. Значение по умолчанию для label — «fast» — это запустит стандартные тесты. Строка «full» запустит полный набор тестов, включая те, которые определены как медленные. Если verbose равно 1 или меньше, тесты будут просто показывать информационные сообщения о запущенных тестах; но если оно больше 1, тесты также будут выдавать предупреждения о недостающих тестах. Поэтому, если вы хотите запустить каждый тест и получить сообщения о модулях, у которых нет тестов:

>>> scipy.test(label='full', verbose=2) # or scipy.test('full', 2)

Наконец, если вас интересует тестирование только подмножества SciPy, например, модуля integrate, используйте следующее:

>>> scipy.integrate.test()

или из командной строки:

$python runtests.py -t scipy/integrate/tests

В остальной части этой страницы вы получите базовое представление о том, как добавлять юнит-тесты в модули SciPy. Для нас крайне важно иметь обширные юнит-тесты, поскольку этот код будут использовать учёные и исследователи, и его разрабатывает большое количество людей по всему миру. Поэтому, если вы пишете пакет, который хотите включить в SciPy, пожалуйста, пишите тесты по мере разработки пакета. Также, поскольку значительная часть SciPy — это наследуемый код, первоначально написанный без юнит-тестов, есть ещё несколько модулей, у которых пока нет тестов. Не стесняйтесь выбрать один из этих модулей и разработать тесты для него, пока читаете это введение.

Написание собственных тестов

Каждый Python-модуль, модуль расширения или подпакет в директории пакета SciPy должен иметь соответствующий test_<name>.py файл. Pytest ищет в этих файлах методы тестов (названные test*) и классы тестов (названные Test*).

Предположим, у вас есть модуль SciPy scipy/xxx/yyy.py, содержащий функцию zzz(). Для тестирования этой функции вы создадите модуль тестов, названный test_yyy.py. Если вам нужно протестировать только один аспект zzz, вы можете просто добавить функцию теста:

def test_zzz():
    assert_(zzz() == 'Hello from zzz')

Чаще всего нам нужно сгруппировать несколько тестов вместе, поэтому мы создаём класс тестов:

from numpy.testing import assert_, assert_raises

# import xxx symbols
from scipy.xxx.yyy import zzz

class TestZzz:
    def test_simple(self):
        assert_(zzz() == 'Hello from zzz')

    def test_invalid_parameter(self):
        assert_raises(...)

Внутри этих методов тестов assert_() и связанные функции используются для проверки того, является ли определённое предположение верным. Если утверждение не выполняется, тест завершается неудачей. Обратите внимание, что встроенная функция Python assert не должна использоваться, потому что она удаляется во время компиляции с -O.

Обратите внимание, что функции или методы test_ не должны иметь документацию, потому что это затрудняет идентификацию теста из вывода запуска набора тестов с verbose=2 (или аналогичной настройкой уровня детализации). Используйте обычные комментарии (#) при необходимости.

Мечение тестов

В качестве альтернативы pytest.mark.<label>, вы можете использовать несколько меток.

Немеченные тесты, подобные тем, что выше, выполняются в стандартном scipy.test() запуске. Если вы хотите пометить свой тест как медленный — и, следовательно, предназначенный для полного scipy.test(label='full') запуска, вы можете пометить его декоратором:

# numpy.testing module includes 'import decorators as dec'
from numpy.testing import dec, assert_

@dec.slow
def test_big(self):
    print 'Big, slow test'

Аналогично для методов:

class test_zzz:
    @dec.slow
    def test_simple(self):
        assert_(zzz() == 'Hello from zzz')

Доступные метки:

  • slow: отмечает тест как требующий длительного времени
  • setastest(tf): обходной путь для обнаружения тестов, когда имя теста не соответствует требованиям
  • skipif(condition, msg=None): пропускает тест, когда eval(condition) — True
  • knownfailureif(fail_cond, msg=None): не будет выполнять тест, если eval(fail_cond) — True, полезно для тестов, которые условно вызывают сегментированную ошибку
  • deprecated(conditional=True): фильтрует предупреждения об устаревании, выдаваемые в тесте
  • paramaterize(var, input): альтернатива pytest.mark.paramaterized

Более простые функции настройки и завершения / методы

Тестирование ищет функции настройки и завершения на уровне модуля или класса по имени, таким образом:

def setup():
    """Module-level setup"""
    print 'doing setup'

def teardown():
    """Module-level teardown"""
    print 'doing teardown'


class TestMe:
    def setup():
        """Class-level setup"""
        print 'doing setup'

    def teardown():
        """Class-level teardown"""
        print 'doing teardown'

Функции настройки и завершения для функций и методов известны как «фикстуры», и их использование не рекомендуется.

Параметрические тесты

Очень удобной функцией тестирования является лёгкое тестирование по диапазону параметров — сложная проблема для стандартных юнит-тестов. Используйте декоратор dec.paramaterize.

Тесты с документацией

Тесты с документацией — удобный способ документирования поведения функции и одновременного тестирования этого поведения. Вывод интерактивной Python-сессии можно включить в строку документации функции, и фреймворк для тестов может запустить пример и сравнить фактический вывод с ожидаемым выводом.

Тесты с документацией можно запустить, добавив аргумент doctests к вызову test(); например, чтобы запустить все тесты (включая тесты с документацией) для numpy.lib:

>>> import numpy as np
>>> np.lib.test(doctests=True)

Тесты с документацией выполняются как будто они находятся в новой Python-инстанции, которая выполнила import numpy as np. Тесты, которые являются частью подпакета SciPy, будут иметь этот подпакет, уже импортированный. Например, для теста в scipy/linalg/tests/, пространство имён будет создано таким образом, что from scipy import linalg уже выполнено.

tests/

Вместо того, чтобы хранить код и тесты в одном каталоге, мы помещаем все тесты для данного подпакета в подкаталог tests/. В нашем примере, если он ещё не существует, вам нужно создать каталог tests/ в scipy/xxx/. Таким образом, путь к test_yyy.py — scipy/xxx/tests/test_yyy.py.

После написания scipy/xxx/tests/test_yyy.py, можно запустить тесты, перейдя в каталог tests/ и набрав:

python test_yyy.py

Или, если вы добавите scipy/xxx/tests/ в путь Python, вы можете запустить тесты интерактивно в интерпретаторе так:

>>> import test_yyy
>>> test_yyy.test()

__init__.py и setup.py

Обычно, однако, добавление каталога tests/ в путь Python нежелательно. Лучше вызвать тест непосредственно из модуля xxx. Для этого просто поместите следующие строки в конец файла __init__.py вашего пакета:

...
def test(level=1, verbosity=1):
    from numpy.testing import Tester
    return Tester().test(level, verbosity)

Вам также нужно добавить каталог с тестами в раздел конфигурации вашего setup.py:

...
def configuration(parent_package='', top_path=None):
    ...
    config.add_subpackage('tests')
    return config
...

Теперь вы можете выполнить следующие действия для тестирования вашего модуля:

>>> import scipy
>>> scipy.xxx.test()

Кроме того, при вызове всего набора тестов SciPy ваши тесты будут найдены и запущены:

>>> import scipy
>>> scipy.test()
# your tests are included and run automatically!

Советы и хитрости

Создание множества похожих тестов

Если у вас есть набор тестов, которые нужно запускать многократно с незначительными вариациями, может быть полезно создать базовый класс, содержащий все общие тесты, а затем создать подкласс для каждой вариации. Несколько примеров этой техники существуют в NumPy; ниже приведены фрагменты одного из них из numpy/linalg/tests/test_linalg.py:

class LinalgTestCase:
    def test_single(self):
        a = array([[1.,2.], [3.,4.]], dtype=single)
        b = array([2., 1.], dtype=single)
        self.do(a, b)

    def test_double(self):
        a = array([[1.,2.], [3.,4.]], dtype=double)
        b = array([2., 1.], dtype=double)
        self.do(a, b)

    ...

class TestSolve(LinalgTestCase):
    def do(self, a, b):
        x = linalg.solve(a, b)
        assert_almost_equal(b, dot(a, x))
        assert_(imply(isinstance(b, matrix), isinstance(x, matrix)))

class TestInv(LinalgTestCase):
    def do(self, a, b):
        a_inv = linalg.inv(a)
        assert_almost_equal(dot(a, a_inv), identity(asarray(a).shape[0]))
        assert_(imply(isinstance(a, matrix), isinstance(a_inv, matrix)))

В данном случае мы хотели протестировать решение задачи линейной алгебры, используя матрицы нескольких типов данных, используя linalg.solve и linalg.inv. Общие тестовые случаи (для матриц одинарной точности, двойной точности и т. д.) собраны в LinalgTestCase.

Известные сбои и пропуск тестов

Иногда вам может потребоваться пропустить тест или пометить его как известный сбой, например, когда набор тестов пишется до кода, который он должен тестировать, или если тест завершается неудачей только на определённой архитектуре.

Для пропуска теста просто используйте skipif:

import pytest

@pytest.mark.skipif(SkipMyTest, reason="Skipping this test because...")
def test_something(foo):
    ...

Тест отмечается как пропущенный, если SkipMyTest имеет ненулевое значение, и сообщение в подробном выводе теста — это второй аргумент, переданный в skipif.

Аналогично, тест можно пометить как известный сбой, используя xfail:

import pytest

@pytest.mark.xfail(MyTestFails, reason="This test is known to fail because...")
def test_something_else(foo):
    ...

Конечно, тест можно безусловно пропустить или пометить как известный сбой, используя skip или xfail без аргумента соответственно.

Итоговое количество пропущенных и известных сбоев отображается в конце запуска теста. Пропущенные тесты отмечаются как 'S' в результатах теста (или 'SKIPPED' для verbose > 1 ), а известные сбои — как 'x' (или 'XFAIL' если verbose > 1).

Тестирование на случайных данных

Тестирование на случайных данных хорошо, но поскольку неудачи тестов предназначены для выявления новых ошибок или регрессий, тест, который проходит большинство раз, но иногда терпит неудачу без изменений кода, бесполезен. Сделайте случайные данные детерминированными, установив seed генератора случайных чисел перед его генерацией. Используйте либо встроенный в Python random.seed(some_number) или numpy.random.seed(some_number) NumPy, в зависимости от источника случайных чисел.

END_OF_DOCUMENT_MARKER ```

В качестве альтернативы можно использовать Hypothesis для генерации произвольных данных. Hypothesis управляет случайными семенами Python и Numpy, предоставляя очень лаконичный и мощный способ описания данных (включая hypothesis.extra.numpy, например, для набора взаимно-расширяемых форм).

Преимущества перед случайной генерацией включают инструменты для воспроизведения и совместного использования сбоев без необходимости фиксированного семени, отчеты о минимальных примерах для каждого сбоя и более совершенные, чем у случайной генерации, методы для воспроизведения ошибок.

© 2005–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.19/reference/testing.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API