Spec-Zone.ru › NumPy 1.15

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

Введение

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

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

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

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

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

$ python runtests.py

SciPy использует фреймворк тестирования из NumPy (в частности, Поддержка тестирования (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 (или аналогичным значением подробности). Используйте обычные комментарии (#) при необходимости.

Иногда бывает удобно запускать test_yyy.py отдельно, поэтому мы добавляем

if __name__ == "__main__":
    run_module_suite()

вниз.

Маркировка тестов

В качестве альтернативы 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.

Doctests

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

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

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

Doctests выполняются так, как если бы они находились в новой инстанции 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.

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

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

Чтобы пропустить тест, просто используйте skipif:

from numpy.testing import dec

@dec.skipif(SkipMyTest, "Skipping this test because...")
def test_something(foo):
    ...

Тест пропускается, если SkipMyTest принимает ненулевое значение, и сообщение в подробном выводе теста – это второй аргумент, переданный skipif. Аналогично, тест можно пометить как известную ошибку, используя knownfailureif:

from numpy.testing import dec

@dec.knownfailureif(MyTestFails, "This test is known to fail because...")
def test_something_else(foo):
    ...

Конечно, тест можно безусловно пропустить или пометить как известную ошибку, передав True в качестве первого аргумента skipif или knownfailureif, соответственно.

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

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

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

© 2005–2019 NumPy Developers
Licensed under the 3-clause BSD License.
https://docs.scipy.org/doc/numpy-1.15.4/reference/testing.html

Spec-Zone.ru

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