Модульные тесты для функций в ноутбуке 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, которую можно использовать для проведения док-тестов и юнит-тестов в ноутбуках. Он также может выполнять блокноты сверху вниз в новом ядре и сообщать о любых ошибках выполнения (проверка работоспособности).