Spec-Zone.ru › NumPy 1.18

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

Введение

До релиза 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(object):
    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_data_dir('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).

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

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

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

Spec-Zone.ru

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