使用外部数据文件的Python单元测试
我有一个在Eclipse中工作的Python项目,我有以下文件结构:使用外部数据文件的Python单元测试,python,unit-testing,Python,Unit Testing,我有一个在Eclipse中工作的Python项目,我有以下文件结构: /Project /projectname module1.py module2.py # etc. /test testModule1.py # etc. testdata.csv 在我的一个测试中,我创建了一个类的实例,将'testdata.csv'作为参数。此对象不打开('testdata.csv')并读取
/Project
/projectname
module1.py
module2.py
# etc.
/test
testModule1.py
# etc.
testdata.csv
在我的一个测试中,我创建了一个类的实例,将'testdata.csv'
作为参数。此对象不打开('testdata.csv')并读取内容
如果我只使用unittest
运行这个单一的测试文件,那么一切都会正常工作,文件会被找到并正确读取。但是,如果我尝试运行所有单元测试(即,右键单击test
目录而不是单个测试文件来运行),我会得到一个错误,该文件找不到
有没有办法绕过这个问题(除了提供一个绝对路径,我不希望这样做)?您的测试不应该直接打开文件,每个测试都应该复制文件并使用其副本。通常我做的是定义
THIS\u DIR=os.path.dirname(os.path.abspath(\uu file\uuu))
在每个测试模块的顶部。然后,不管您在哪个工作目录中,相对于测试模块所在的位置,文件路径总是相同的
然后在我的测试(或测试设置)中使用类似的内容:
或者在您的情况下,由于数据源位于测试目录中:
my_data_path = os.path.join(THIS_DIR, 'testdata.csv')
访问文件系统的单元测试通常不是一个好主意。这是因为测试应该是自包含的,通过使您的测试数据位于测试外部,csv文件属于哪个测试或者即使它仍在使用中,也不再明显 一个更好的解决方案是修补
open
,并使其返回类似文件的对象
from unittest import TestCase
from unittest.mock import patch, mock_open
from textwrap import dedent
class OpenTest(TestCase):
DATA = dedent("""
a,b,c
x,y,z
""").strip()
@patch("builtins.open", mock_open(read_data=DATA))
def test_open(self):
# Due to how the patching is done, any module accessing `open' for the
# duration of this test get access to a mock instead (not just the test
# module).
with open("filename", "r") as f:
result = f.read()
open.assert_called_once_with("filename", "r")
self.assertEqual(self.DATA, result)
self.assertEqual("a,b,c\nx,y,z", result)
在我看来,处理这些情况的最佳方法是通过控制反转编程 在下面的两个部分中,我主要展示无反转控制解决方案的外观。第二部分展示了控制反转的解决方案,以及如何在没有模拟框架的情况下测试此代码 最后,我陈述了一些个人的优点和缺点,这些优点和缺点根本没有正确和/或完整的意图。请随意评论,以便进行补充和更正 无控制反转(无依赖注入) 您有一个使用python中std
open
方法的类
class UsesOpen(object):
def some_method(self, path):
with open(path) as f:
process(f)
# how the class is being used in the open
def main():
uses_open = UsesOpen()
uses_open.some_method('/my/path')
在这里,我在代码中显式地使用了open
,因此为其编写测试的唯一方法是使用显式测试数据(文件)或使用Dunes建议的模拟框架。
但还有另一种方法:
我的建议:控制反转(使用依赖注入)
现在我以不同的方式改写了课程:
class UsesOpen(object):
def __init__(self, myopen):
self.__open = myopen
def some_method(self, path):
with self.__open(path) as f:
process(f)
# how the class is being used in the open
def main():
uses_open = UsesOpen(open)
uses_open.some_method('/my/path')
在第二个示例中,我将open
的依赖项注入构造函数(构造函数依赖项注入)
控制反转的书写测试
现在,我可以轻松编写测试并在需要时使用我的测试版本open
:
EXAMPLE_CONTENT = """my file content
as an example
this can be anything"""
TEST_FILES = {
'/my/long/fake/path/to/a/file.conf': EXAMPLE_CONTENT
}
class MockFile(object):
def __init__(self, content):
self.__content = content
def read(self):
return self.__content
def __enter__(self):
return self
def __exit__(self, type, value, tb):
pass
class MockFileOpener(object):
def __init__(self, test_files):
self.__test_files = test_files
def open(self, path, *args, **kwargs):
return MockFile(self.__test_files[path])
class TestUsesOpen(object):
def test_some_method(self):
test_opener = MockFileOpener(TEST_FILES)
uses_open = UsesOpen(test_opener.open)
# assert that uses_open.some_method('/my/long/fake/path/to/a/file.conf')
# does the right thing
赞成/反对
亲依赖注入
- 无需学习模拟测试框架
- 完全控制必须伪造的类和方法
- 另外,一般来说,更改和改进代码更容易
- 代码质量通常会提高,这是最重要的 factors能够尽可能轻松地响应变化
- 使用依赖项注入和依赖项注入框架 通常是一种受人尊敬的项目工作方式
- 一般来说,需要编写更多的代码
- 在测试中不如通过@patch修补类那么短
- 构造函数可能会因依赖项而过载
- 您需要以某种方式学习使用依赖注入
的文件名?或者看看我假设测试在项目根目录中运行的位置。哦,使用os.getcwd来避免绝对路径是个不错的主意。如果不是从Project
运行的话,我会尝试一下,它会崩溃的!现在我看一下,也许从os.path.dirname(\uuu file\uuuu)
开始更新会更好,并且仍然会传递CI:(文档显然已损坏,但这似乎不相关!)谢谢,我认为这将非常有效。这仍然留下了找出原始文档位置的问题!您可以尝试这样做:sys.path.append(os.path.abspath('/home/user/Project/FolderWithTests')),这完全没有意义。既然您从一个绝对路径开始(记住,OP一开始并不需要这个路径),为什么要将它传递给abspath
?我通常会把在运行时修改sys.path
作为最后的手段——通常会有更简洁的解决方案。我想你是指我的conf.py
,它采用相对路径并使它们成为绝对路径,这不是你的建议。请不要给出不好的建议。出于好奇,你或任何人能详细解释一下这句话吗?为什么在我的测试中直接打开一个文件是不好的做法?那么更大的(在我的示例中是1.5MB)二进制文件呢?我需要测试一个解析二进制数据文件的函数。我的第一个问题是为什么单元测试需要1.5MB的数据?从外部的角度来看,很难验证测试是否正确,或者如果测试在稍后的日期中断,该怎么办。如果您确实需要这些数据,那么它听起来更像是一个集成测试,而不是一个试图测试代码的一个子组件的单元测试。集成测试可以不封装(使用文件系统或数据库等),只需将它们与单元测试分开即可。库的目的是加载大小恰好为1.5MB的二进制数据文件。它是一个文件格式解析器。不过,谢谢你的回答:我会尝试将测试分开,但这并不能解决很多问题。我有一些不访问文件系统的单元测试和一些访问文件系统的集成测试
EXAMPLE_CONTENT = """my file content
as an example
this can be anything"""
TEST_FILES = {
'/my/long/fake/path/to/a/file.conf': EXAMPLE_CONTENT
}
class MockFile(object):
def __init__(self, content):
self.__content = content
def read(self):
return self.__content
def __enter__(self):
return self
def __exit__(self, type, value, tb):
pass
class MockFileOpener(object):
def __init__(self, test_files):
self.__test_files = test_files
def open(self, path, *args, **kwargs):
return MockFile(self.__test_files[path])
class TestUsesOpen(object):
def test_some_method(self):
test_opener = MockFileOpener(TEST_FILES)
uses_open = UsesOpen(test_opener.open)
# assert that uses_open.some_method('/my/long/fake/path/to/a/file.conf')
# does the right thing