当前位置: 首页 > 科技观察

Jupyter笔记本中的单元测试

时间:2023-03-13 15:44:58 科技观察

我们都知道我们应该在开发过程中编写单元测试,而且我们中的许多人确实这样做了。这对于生产代码、库代码或由于测试驱动的开发过程尤其如此。通常,Jupyter笔记本用于数据探索,因此用户可能不会选择(或不需要)为他们的代码编写单元测试,因为当他们在Jupyter中运行时,他们通常会查看每个单元格的结果,然后得出结论,继续然后。然而,根据我的经验,Jupyter通常会发生的情况是,Jupyter中的代码很快就超出了数据探索的范围,并变得对进一步的工作有用。或者,Jupyter本身可能会产生需要定期运行的有用结果。也许代码需要维护并与外部数据源集成。那么确保笔记本中的代码可以被测试和验证就变得很重要了。在这种情况下,我们对Jupyter代码进行单元测试的选项是什么?在本文中,我将描述在Jupyter笔记本中对Python代码进行单元测试的几个选项。也许只是不这样做?Jupyternotebook单元测试的第一个选择是什么都不做。通过这种方式,我并不是说不要对代码进行单元测试,而是将其从笔记本中提取到一个单独的Python模块中,然后可以将其重新导入到笔记本中。该代码应该以与通常单元测试单元代码相同的方式进行测试,无论是使用unittest、pytest、doctest还是其他单元测试框架。本文不会详细介绍所有这些框架,但对于Python开发人员来说,一个不错的选择是不要在他们的JupyterNotebook中进行测试,而是使用各种适用于Python代码的测试框架,并在开发期间尽快使用将代码移动到外部模块。在笔记本中测试如果您最终决定将代码保存在Jupyter笔记本中,实际上有一些单元测试选项。在查看其中一些内容之前,让我们设置一个您可能会在Jupyter笔记本中遇到的代码示例。假设您的笔记本从API中提取一些数据,从中计算出一些结果,然后生成一些图表和其他保存在其他地方的数据摘要。也许有一个函数可以生成正确的APIURL,我们想对该函数进行单元测试。这个函数有一些逻辑可以根据报告的日期更改URL格式。这是调试版本。importdatetimeimportdateutildefmake_url(date):"""ReturntheurlforourAPIcallbasedondate."""ifisinstance(date,str):date=dateutil.parser.parse(date).date()elifnotisinstance(date,datetime.date):raiseValueError("mustbeadate")ifdate>=datetime.date(2020,1,1):returnf"https://api.example.com/v2/{date.year}/{date.month}/{date.day}"else:returnf"https://api.example.com/v1/{date:%Y-%m-%d}》使用unittest进行单元测试通常我们在使用unittest进行测试时,会将测试方法放在单独的测试模块中,或者可能将这些方法混合到主模块中。然后,我们需要执行unittest.main方法,可能是__main__守卫中的默认方法。我们基本上可以在Jupyternotebook中做同样的事情。我们可以创建一个unitest.TestCase类,执行所需的测试,然后在任何单元格中执行单元测试。您只需要保存unittest.main方法的输出并检查错误。importunittestclassTestUrl(unittest.TestCase):deftest_make_url_v2(self):date=datetime.date(2020,1,1)self.assertEqual(make_url(date),"https://api.example.com/v2/2020/1/1")deftest_make_url_v1(self):date=datetime.date(2019,12,31)self.assertEqual(make_url(date),"https://api.example.com/v1/2019-12-31")res=unittest.main(argv=[''],verbosity=3,exit=False)#ifwewantournotebooktostopprocessingduetofailures,weneedacellitselftofailassertlen(res.result.failures)==0test_make_url_v1(__main__.TestUrl)...oktest_make_url_v2(__main__.TestUrl)..。好的----------------------------------------------Ran2testsin0.001sOK如果您不介意在笔记本中混合代码和测试,结果证明它非常简单并且运行良好。使用doctest进行单元测试在代码中包含测试的另一种方法是使用doctest。Doctest使用特殊格式的代码文档,其中包括我们的测试和预期结果。下面是一种更新的方法,其中包括此特定代码的文档,包括正例和负例。这是一种在一个地方测试和记录代码的简单方法,通常用于python模块,其中主头文件将只运行doc测试,如下所示:if__name__==__main__:doctest.testmod()因为我们在笔记本中,所以只需将其添加到定义代码的单元格中,它也会起作用。首先,这是我们更新后的带有doctest注释的make_url方法。defmake_url(date):"""ReturntheurlforourAPIcallbasedondate.>>>make_url("1/1/2020")'https://api.example.com/v2/2020/1/1'>>>make_url("1-1-x1")追溯(mostrecentcallast):...dateutil.parser._parser.ParserError:Unknownstringformat:1-1-x1>>>make_url(“1/1/20001”)追溯(mostrecentcallast):...dateutil.parser._parser.ParserError:year20001isoutofrange:1/1/20001>>>make_url(datetime.date(2020,1,1))'https://api.example.com/v2/2020/1/1'>>>make_url(datetime.date(2019,12,31))'https://api.example.com/v1/2019-12-31'"""ifisinstance(date,str):date=dateutil.parser。解析(日期).date()elifnotisinstance(date,datetime.date):raiseValueError(“mustbeadate”)ifdate>=datetime.date(2020,1,1):returnf“https://api.example.com/v2/{date.year}/{date.month}/{date.day}"else:returnf"https://api.example.com/v1/{date:%Y-%m-%d}"importdoctestdoctest.吨estmod()TestResults(failed=0,attempted=5)使用testbook进行单元测试。testbook项目是笔记本单元测试的另一种方式。它允许您使用纯Python代码从笔记本外部引用笔记本。这允许您在单独的Python模块中使用您喜欢的任何测试框架(例如pytest或unittest)。您可能会遇到这样一种情况,即允许用户修改和更新笔记本代码是保持代码更新并为最终用户提供灵活性的最佳方式。但您可能仍想单独测试和验证代码。Testbook使它成为一个选项。首先,您必须在您的环境中安装它:pipinstalltestbook或在您的笔记本中:%pipinstalltestbook现在,在一个单独的python文件中,您可以导入笔记本代码并在那里进行测试。在该文件中,您将创建类似于以下的代码,然后使用您喜欢的任何单元测试框架来实际进行单元测试。您可以在Python文件(例如jupyter_unit_tests.py)中创建以下代码。importdatetimeimporttestbook@testbook.testbook('./jupyter_unit_tests.ipynb',execute=True)deftest_make_url(tb):func=tb.ref("make_url")date=datetime.date(2020,1,2)assertmake_url(date)=="https://api.example.com/v2/2020/1/1"在这种情况下,您现在可以使用任何单元测试框架来运行测试。例如,使用pytest,您可以简单地运行以下命令:pytestjupyter_unit_tests.py这作为一个正常的单元测试,测试应该通过。然而,在写这篇文章的时候,我意识到testbook代码对将参数从单元测试传递回notebook内核进行测试的支持有限。参数是JSON序列化的,当前代码知道如何处理各种Python类型。但是,它不会将日期时间作为对象传递,而是作为字符串传递。由于我们的代码试图将字符串解析为日期(在我修改它之后),所以它有效。换句话说,上面的单元测试不是将datetime.date传递给make_url方法,而是传递一个字符串(2020-01-02),然后将其解析为日期。如何将单元测试中的日期传递到笔记本代码中?您有多种选择。首先,您可以在笔记本中创建一个日期对象,仅用于测试目的,然后在您的单元测试中引用它。testdate1=datetime.date(2020,1,1)#forunittest然后您可以编写单元测试以在测试中使用该变量。第二种选择是将Python代码写入笔记本,然后在单元测试中重新引用它。这两个选项都显示在外部单元测试的最终版本中。只需将它保存在jupyter_unit_tests.py上,然后使用你最喜欢的单元测试框架运行它。importdatetimeimporttestbook@testbook.testbook('./jupyter_unit_tests.ipynb',execute=True)deftest_make_url(tb):f=tb.ref("make_url")d="2020-01-02"assertf(d)=="https://api.example.com/v2/2020/1/2"#notethatthisisactuallyconvertedtoastringd=datetime.date(2020,1,2)assertf(d)=="https://api.example.com/v2/2020/1/2"#thisonewillbetestingthedatefunctionalityd2=tb.ref("testdate1")assertf(d2)=="https://api.example.com/v2/2020/1/1"#thisonewillinjectsimilarcodeasabove,然后使用ittb.inject("d3=datetime.date(2020,2,3)")d3=tb.ref("d3")assertf(d3)=="https://api.example.com/v2/2020/2/3"总结如下,无论您是单元测试的纯粹主义者,还是只想在笔记本中添加一些单元测试,您都可以考虑多种选择。不要让笔记本的使用妨碍在测试代码时做正确的事情。