Модульные тесты для функций в ноутбуке Jupyter?

У меня есть ноутбук Jupyter, который я планирую многократно запускать. В нем есть функции, структура кода такова:

def construct_url(data):
    ...
    return url

def scrape_url(url):
    ... # fetch url, extract data
    return parsed_data

for i in mylist: 
    url = construct_url(i)
    data = scrape_url(url)
    ... # use the data to do analysis

Я бы хотел написать тесты для construct_url и scrape_url. Какой самый разумный способ сделать это?

Некоторые подходы, которые я рассмотрел:

  • Переместите функции в файл утилиты и напишите тесты для этого файла утилиты в некоторой стандартной тестовой библиотеке Python. Возможно, лучший вариант, хотя это означает, что не весь код отображается в ноутбуке.
  • Записывайте утверждения внутри самого ноутбука, используя тестовые данные (добавляет шум к ноутбуку).
  • Используйте специализированное тестирование Jupyter для проверки содержимого ячеек (не думайте, что это работает, потому что содержимое ячеек изменится).

Ответ 1

Можно использовать стандартные инструменты тестирования Python, такие как doctest или unittest, непосредственно в ноутбуке.

Doctest

Ячейка для ноутбука с функцией и тестовым примером в docstring:

def add(a, b):
    '''
    This is a test:
    >>> add(2, 2)
    5
    '''
    return a + b

Ячейка ноутбука (последняя в записной книжке), которая запускает все тестовые примеры в док-строках:

import doctest
doctest.testmod(verbose=True)

Вывод:

Trying:
    add(2, 2)
Expecting:
    5
**********************************************************************
File "__main__", line 4, in __main__.add
Failed example:
    add(2, 2)
Expected:
    5
Got:
    4
1 items had no tests:
    __main__
**********************************************************************
1 items had failures:
   1 of   1 in __main__.add
1 tests in 2 items.
0 passed and 1 failed.
***Test Failed*** 1 failures.

Unit тест

Ячейка для ноутбука с функцией:

def add(a, b):
    return a + b

Ячейка ноутбука (последняя в записной книжке), содержащая тестовый пример. Последняя строка в ячейке запускает тестовый пример, когда выполняется ячейка:

import unittest

class TestNotebook(unittest.TestCase):

    def test_add(self):
        self.assertEqual(add(2, 2), 5)


unittest.main(argv=[''], verbosity=2, exit=False)

Вывод:

test_add (__main__.TestNotebook) ... FAIL

======================================================================
FAIL: test_add (__main__.TestNotebook)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-15-4409ad9ffaea>", line 6, in test_add
    self.assertEqual(add(2, 2), 5)
AssertionError: 4 != 5

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

Отладка неудачного теста

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

import pdb; pdb.set_trace()

Например:

def add(a, b):
    '''
    This is the test:
    >>> add(2, 2)
    5
    '''
    import pdb; pdb.set_trace()
    return a + b

В этом примере при следующем запуске doctest выполнение будет остановлено непосредственно перед оператором return, и начнется отладчик Python (pdb). Вы получите подсказку pdb непосредственно в записной книжке, которая позволит вам проверять значения a и b, переходить по строкам и т.д.

Я создал блокнот Jupyter для экспериментов с методами, которые я только что описал.

Ответ 2

На мой взгляд, лучший способ провести юнит-тесты в блокноте Jupyter - это следующий пакет: https://github.com/JoaoFelipe/ipython-unittest

Пример из пакета документов:

%%unittest_testcase
def test_1_plus_1_equals_2(self):
    sum = 1 + 1
    self.assertEqual(sum, 2)

def test_2_plus_2_equals_4(self):
    self.assertEqual(2 + 2, 4)

Success
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Ответ 3

После небольшого исследования я нашел свое собственное решение, где у меня есть свой собственный код тестирования, похожий на этот

def red(text):
    print('\x1b[31m{}\x1b[0m'.format(text))

def assertEquals(a, b):
    res = a == b
    if type(res) is bool:
        if not res:
            red('"{}" is not "{}"'.format(a, b))
            return
    else:
        if not res.all():
            red('"{}" is not "{}"'.format(a, b))
            return

    print('Assert okay.')

Что это делает

  • Проверьте, равно ли a b.
  • Если они разные, аргументы отображаются красным цветом.
  • Если они одинаковые, то это говорит "хорошо".
  • Если результатом сравнения является массив, он проверяет, является ли all() истинным.

Я положил функцию на верхней части моего ноутбука, и я тестирую что-то вроде этого

def add(a, b):
    return a + b

assertEquals(add(1, 2), 3)
assertEquals(add(1, 2), 2)
assertEquals([add(1, 2), add(2, 2)], [3, 4])

---

Assert okay.
"3" is not "2"  # This is shown in red.
Assert okay.

Плюсы этого подхода

  • Я могу проверить ячейку за ячейкой и увидеть результат, как только я изменю что-то в функции.
  • Мне не нужно добавлять дополнительный код, например doctest.testmod(verbose=True) который я должен добавить, если я использую doctest.
  • Сообщения об ошибках просты.
  • Я могу настроить свой тестовый (утверждающий) код.

Ответ 4

Учитывая ваш контекст, лучше всего написать doctests для construct_url & scrape_url внутри ячеек ноутбука, как это,

def construct_url(data):
    '''
    >>> data = fetch_test_data_from_somewhere()
    >>> construct_url(data)
    'http://some-constructed-url/'
    '''

    ... 
    <actual function>
    ...

Затем вы можете выполнить их с другой ячейкой внизу:

import doctest
doctest.testmod(verbose=True)

Я также создал treon, тестовую библиотеку для ноутбуков Jupyter, которую можно использовать для проведения док-тестов и юнит-тестов в ноутбуках. Он также может выполнять блокноты сверху вниз в новом ядре и сообщать о любых ошибках выполнения (проверка работоспособности).