使用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 fpfilepath。您可以修复该问题,但无论哪种方式,您都不会看到 read_file 的任何错误,除非您通过迭代它来使用返回的生成器。

EDIT:我不建议在这样的生成器中使用 with 语句。原因是不能保证文件一定会被关闭。

要理解这一点,请考虑 with 的目的是什么。在上下文管理器之前,您将打开一个文件,如

f = open(filename)
f.read(4)
# etc
f.close()

问题在于,如果 openclose 之间发生某些事情(例如异常或 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函数)