使用pytest测试文件读取功能时出错
Error testing file reading function with pytest
我有一个简单的功能:
def read_file(fp):
with open(fp) as fr:
for line in fr.readlines():
yield line
当我在一个不存在的文件上运行这个函数时,我得到:
FileNotFoundError: [Errno 2] No such file or directory: 'idontexist.txt'
在另一个文件中,我正在尝试使用 pytest
测试此功能:
import pytest
from utils import read_file
def test_file_not_exist():
filepath = 'idontexist.txt'
with pytest.raises(FileNotFoundError):
read_file(filepath)
但是,运行ning pytest
,我收到消息:
E Failed: DID NOT RAISE <class 'FileNotFoundError'>
为什么这个测试没有通过?
您正在创建生成器函数。调用生成器函数 returns 一个生成器对象:
>>> def read_file(fp):
... with open(filepath) as fr:
... for line in fr.readlines():
... yield line
...
>>> read_file('asd')
<generator object read_file at 0x10554ee08>
打开调用(应该引发 FileNotFoundError)将不会被调用,直到您遍历生成器。然后你会看到
>>> g = read_file('asd')
>>> for x in g:
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in read_file
NameError: name 'filepath' is not defined
我猜你在发帖之前 half-changed fp
到 filepath
。您可以修复该问题,但无论哪种方式,您都不会看到 read_file
的任何错误,除非您通过迭代它来使用返回的生成器。
EDIT:我不建议在这样的生成器中使用 with
语句。原因是不能保证文件一定会被关闭。
要理解这一点,请考虑 with
的目的是什么。在上下文管理器之前,您将打开一个文件,如
f = open(filename)
f.read(4)
# etc
f.close()
问题在于,如果 open
和 close
之间发生某些事情(例如异常或 return
等),文件可能无法关闭。您可以使用 try/finally
解决此问题,即
f = open(filename)
try:
f.read(4)
# etc
finally:
f.close()
这很麻烦,所以我们有 with
语句将其缩短为
with open(filename) as f:
f.read(4)
# etc
这很好,因为它减少了混乱,并且在文件未关闭的情况下无法离开 with
语句。但是,当您在像上面的 read_file
这样的生成器中执行此操作时,有人可能会用
调用生成器
for line in read_file(filename):
if line.startswith('#'):
break
现在在 break
之后,生成器在 yield
处暂停,它无法知道它不会再次迭代,所以它会在那里等待。 with
块内的 yield
允许您在不关闭文件的情况下离开上下文管理器。 (使用 try/finally
时也会出现同样的问题,但在那种情况下可能更明显。)即使您知道不会 break
,循环体中的异常也会产生相同的效果。
在这种情况下,由于 CPython 中的 ref-counting GC,文件将 可能 关闭:收集生成器时,它将关闭,终止 with
块并因此关闭文件时抛出异常。这并不比让 GC 直接收集文件对象 f
(也通过 file.__del__
关闭文件)好多少。
简单的规则是:
Don't yield
inside a with
statement
这意味着您通常应该在生成器外部使用 with
语句。所以你做一些像
def read_file(f):
for line in f.readlines():
yield line
# Control resource at top level
with open(filename) as fin:
for line in read_file(fin) # pass the resource to generator
# do something with line
另一点:迭代器的全部意义在于它们使我们不必执行诸如将整个文件读入内存之类的操作。因此,与其调用 readlines()
将 while 文件读入内存,不如直接遍历文件,一次只读取一行。通过这两项更改,您的函数如下所示:
def read_file(f):
for line in f:
yield line
甚至:
def read_file(f):
yield from f
就迭代器而言,这只是恒等函数,所以它是多余的,可以删除。因此,无论您在哪里使用 read_lines
函数,您都可以只使用
with open(filename) as fin:
for line in fin:
# do stuff
(即代码中不再有read_lines
函数)
我有一个简单的功能:
def read_file(fp):
with open(fp) as fr:
for line in fr.readlines():
yield line
当我在一个不存在的文件上运行这个函数时,我得到:
FileNotFoundError: [Errno 2] No such file or directory: 'idontexist.txt'
在另一个文件中,我正在尝试使用 pytest
测试此功能:
import pytest
from utils import read_file
def test_file_not_exist():
filepath = 'idontexist.txt'
with pytest.raises(FileNotFoundError):
read_file(filepath)
但是,运行ning pytest
,我收到消息:
E Failed: DID NOT RAISE <class 'FileNotFoundError'>
为什么这个测试没有通过?
您正在创建生成器函数。调用生成器函数 returns 一个生成器对象:
>>> def read_file(fp):
... with open(filepath) as fr:
... for line in fr.readlines():
... yield line
...
>>> read_file('asd')
<generator object read_file at 0x10554ee08>
打开调用(应该引发 FileNotFoundError)将不会被调用,直到您遍历生成器。然后你会看到
>>> g = read_file('asd')
>>> for x in g:
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in read_file
NameError: name 'filepath' is not defined
我猜你在发帖之前 half-changed fp
到 filepath
。您可以修复该问题,但无论哪种方式,您都不会看到 read_file
的任何错误,除非您通过迭代它来使用返回的生成器。
EDIT:我不建议在这样的生成器中使用 with
语句。原因是不能保证文件一定会被关闭。
要理解这一点,请考虑 with
的目的是什么。在上下文管理器之前,您将打开一个文件,如
f = open(filename)
f.read(4)
# etc
f.close()
问题在于,如果 open
和 close
之间发生某些事情(例如异常或 return
等),文件可能无法关闭。您可以使用 try/finally
解决此问题,即
f = open(filename)
try:
f.read(4)
# etc
finally:
f.close()
这很麻烦,所以我们有 with
语句将其缩短为
with open(filename) as f:
f.read(4)
# etc
这很好,因为它减少了混乱,并且在文件未关闭的情况下无法离开 with
语句。但是,当您在像上面的 read_file
这样的生成器中执行此操作时,有人可能会用
for line in read_file(filename):
if line.startswith('#'):
break
现在在 break
之后,生成器在 yield
处暂停,它无法知道它不会再次迭代,所以它会在那里等待。 with
块内的 yield
允许您在不关闭文件的情况下离开上下文管理器。 (使用 try/finally
时也会出现同样的问题,但在那种情况下可能更明显。)即使您知道不会 break
,循环体中的异常也会产生相同的效果。
在这种情况下,由于 CPython 中的 ref-counting GC,文件将 可能 关闭:收集生成器时,它将关闭,终止 with
块并因此关闭文件时抛出异常。这并不比让 GC 直接收集文件对象 f
(也通过 file.__del__
关闭文件)好多少。
简单的规则是:
Don't
yield
inside awith
statement
这意味着您通常应该在生成器外部使用 with
语句。所以你做一些像
def read_file(f):
for line in f.readlines():
yield line
# Control resource at top level
with open(filename) as fin:
for line in read_file(fin) # pass the resource to generator
# do something with line
另一点:迭代器的全部意义在于它们使我们不必执行诸如将整个文件读入内存之类的操作。因此,与其调用 readlines()
将 while 文件读入内存,不如直接遍历文件,一次只读取一行。通过这两项更改,您的函数如下所示:
def read_file(f):
for line in f:
yield line
甚至:
def read_file(f):
yield from f
就迭代器而言,这只是恒等函数,所以它是多余的,可以删除。因此,无论您在哪里使用 read_lines
函数,您都可以只使用
with open(filename) as fin:
for line in fin:
# do stuff
(即代码中不再有read_lines
函数)