Spec-Zone.ru › NumPy 1.17

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

Введение

До релиза 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–2019 NumPy Developers
Licensed under the 3-clause BSD License.
https://docs.scipy.org/doc/numpy-1.17.0/reference/testing.html

Spec-Zone.ru

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