pytest-内置fixture
4 内置 Fixture
4.1 使用 tmpdir 和 tmpdir_factory
内置的 tmpdir 和 tmpdir_factory 负责在测试开始运行前创建临时文件目录,并在测试结束后删除。
如果测试代码要对文件进行读/写操作,那可以使用 tmpdir 或 tmpdir_factory 来创建文件或目录。单个测试使用 tmpdir,多个测试使用 tmpdir_factory。
tmpdir 的作用范围是函数级别,tmpdir_factory 的作用范围是会话级别。单个测试需要临时目录或文件应该使用 tmpdir,如果每个测试函数都要重新创建目录或文件,也请使用 tmpdir。
1 def test_tmpdir(tmpdir): 2 # tmpdir already has a path name associated with it 3 # join() extends the path to include a filename 4 # the file is created when it's written to 5 a_file = tmpdir.join('something.txt') 6 7 # you can create directories 8 a_sub_dir = tmpdir.mkdir('anything') 9 10 # you can create files in directories (created when written) 11 another_file = a_sub_dir.join('something_else.txt') 12 13 # this write creates 'something.txt' 14 a_file.write('contents may settle during shipping') 15 16 # this write creates 'anything/something_else.txt' 17 another_file.write('something different') 18 19 # you can read the files as well 20 assert a_file.read() == 'contents may settle during shipping' 21 assert another_file.read() == 'something different' 22 23 24 def test_tmpdir_factory(tmpdir_factory): 25 # you should start with making a directory 26 # a_dir acts like the object returned from the tmpdir fixture 27 a_dir = tmpdir_factory.mktemp('mydir') 28 29 # base_temp will be the parent dir of 'mydir' 30 # you don't have to use getbasetemp() 31 # using it here just to show that it's available 32 base_temp = tmpdir_factory.getbasetemp() 33 print('base:', base_temp) 34 35 # the rest of this test looks the same as the 'test_tmpdir()' 36 # example except I'm using a_dir instead of tmpdir 37 38 a_file = a_dir.join('something.txt') 39 a_sub_dir = a_dir.mkdir('anything') 40 another_file = a_sub_dir.join('something_else.txt') 41 42 a_file.write('contents may settle during shipping') 43 another_file.write('something different') 44 45 assert a_file.read() == 'contents may settle during shipping' 46 assert another_file.read() == 'something different'
在其他作用范围内使用临时目录
tmpdir_factory 的作用范围是会话级别的,tmpdir 的作用范围是函数级别的。如果需要模块或类级别作用范围的目录,可以利用 tmpdir_factory 再创建一个 fixture。
假定有一个测试模块,其中有很多测试用例要读取一个JSON文件,我们可以在模块本身或 conftest.py 中创建一个作用范围是模块级别的 fixture,用于配置该文件。
1 #-----conftest.py 2 3 @pytest.fixture(scope='module') 4 def author_file_json(tmpdir_factory): 5 """Write some authors to a data file.""" 6 python_author_data = { 7 'Ned': {'City': 'Boston'}, 8 'Brian': {'City': 'Portland'}, 9 'Luciano': {'City': 'Sau Paulo'} 10 } 11 12 file = tmpdir_factory.mktemp('data').join('author_file.json') 13 print('file:{}'.format(str(file))) 14 15 with file.open('w') as f: 16 json.dump(python_author_data, f) 17 return file 18 19 20 #-----test_authors.py 21 def test_brian_in_portland(author_file_json): 22 """A test that uses a data file.""" 23 with author_file_json.open() as f: 24 authors = json.load(f) 25 assert authors['Brian']['City'] == 'Portland' 26 27 28 def test_all_have_cities(author_file_json): 29 """Same file is used for both tests.""" 30 with author_file_json.open() as f: 31 authors = json.load(f) 32 for a in authors: 33 assert len(authors[a]['City']) > 0
如果一个测试数据文件能服务于多个测试用例,那么就没必要为每个用例创建一个数据文件。
4.2 使用 pytestconfig
内置的 pytestconfig 可以通过命令行参数、选项、配置文件、插件、运行目录等方式来控制 pytest。pytestconfig 是 request.config 的快捷方式,它在 pytest 文档里有时候被称为“pytest配置对象”。
暂时用处还没挖掘,待后续补充
4.3 使用 cache
cache 的作用是存储一段测试会话的信息,在下一段测试会话中使用。使用 pytest 内置的 --last-failed 和 --failed-first 标识可以很好地展示 cache 的功能。
- --cache-show:show cache contents, don't perform collection or tests
- --cache-clear:remove all cache contents at start of test run
1 #----test_few_failtures.py 2 3 testdata = [ 4 # x, y, expected 5 (1.01, 2.01, 3.02), 6 (1e25, 1e23, 1.1e25), 7 (1.23, 3.21, 4.44), 8 (0.1, 0.2, 0.3), 9 (1e25, 1e24, 1.1e25) 10 ] 11 12 13 @pytest.mark.parametrize("x,y,expected", testdata) 14 def test_a(x, y, expected): 15 """Demo approx().""" 16 sum_ = x + y 17 assert sum_ == approx(expected)
运行这个测试函数,使用 --verbose 标识来显示函数名称,可以直观看到有个函数测试失败。
可以使用 --cache-show 标识来显示存储的信息,可以看到在项目的 ./.pytest_cache/v/cache 目录下存储了三个文件,其中 lastfailed 是存储上一次测试失败的信息。
我们来创建一个 fixture,记录测试的耗时,并存储到 cache 里,如果接下来的测试耗时大于之前的两倍,就抛出超时异常。
cache的接口很简单
- cache.get(key, default)
- cache.set(key, value)
习惯上,键名以应用名字或插件名字开始,接着是/,然后是分隔开的键字符串。键值可以是任何可转化成JSON的东西,因为是 .cache 目录里是用 JSON 格式存储的。
1 #----test_slower.py 2 3 @pytest.fixture(autouse=True) 4 def check_duration(request, cache): 5 key = 'duration/' + request.node.nodeid.replace(':', '_') 6 # nodeid's can have colons 7 # keys become filenames within .cache 8 # replace colons with something filename safe 9 start_time = datetime.datetime.now() 10 yield 11 stop_time = datetime.datetime.now() 12 this_duration = (stop_time - start_time).total_seconds() 13 last_duration = cache.get(key, None) 14 cache.set(key, this_duration) 15 if last_duration is not None: 16 errorstring = "test duration over 2x last duration" 17 assert this_duration <= last_duration * 2, errorstring 18 19 20 @pytest.mark.parametrize('i', range(5)) 21 def test_slow_stuff(i): 22 time.sleep(random.random())
第一次执行时使用了 --cache-clear,用于清空缓存
可以观察到 ./.pytest_cache/v/duration 目录下存了几个文件,duration/test_slower.py__test_slow_stuff[0] 和 代码中的 key=duration/' + request.node.nodeid.replace(':', '_') 是一致的。
每个文件里存的其实就是 键值对中的值
可以用 --cache-show 看看 cache 里存储了什么信息。
接下来的每个测试都将读/写 cache,可以把原先的 fixture 拆分成两个小 fixture:一个作用范围是函数级别,用于测量运行时间;另一个作用范围是会话级别,用来读/写cache。
重构后的代码
1 Duration = namedtuple('Duration', ['current', 'last']) 2 3 4 @pytest.fixture(scope='session') 5 def duration_cache(request): 6 key = 'duration/testdurations' 7 d = Duration({}, request.config.cache.get(key, {})) 8 yield d 9 request.config.cache.set(key, d.current) 10 11 12 @pytest.fixture(autouse=True) 13 def check_duration(request, duration_cache): 14 d = duration_cache 15 nodeid = request.node.nodeid 16 start_time = datetime.datetime.now() 17 yield 18 duration = (datetime.datetime.now() - start_time).total_seconds() 19 d.current[nodeid] = duration 20 if d.last.get(nodeid, None) is not None: 21 errorstring = "test duration over 2x last duration" 22 assert duration <= (d.last[nodeid] * 2), errorstring 23 24 25 @pytest.mark.parametrize('i', range(5)) 26 def test_slow_stuff(i): 27 time.sleep(random.random())
4.4 使用 capsys
pytest 内置的 capsys 有两个功能:允许使用代码读取 stdout 和 stderr;可以临时禁制抓取日志输出。
假设某个函数要把欢迎信息输出到 stdout,你不能使用返回值来测试它,只能测试 stdout,这时可以使用 capsys 来测试。
1 def greeting(name): 2 print('Hi, {}'.format(name)) 3 4 5 def test_greeting(capsys): 6 greeting('Earthling') 7 out, err = capsys.readouterr() 8 assert out == 'Hi, Earthling\n' 9 assert err == '' 10 11 greeting('Brian') 12 greeting('Nerd') 13 out, err = capsys.readouterr() 14 assert out == 'Hi, Brian\nHi, Nerd\n' 15 assert err == '' 16 17 18 def yikes(problem): 19 print('YIKES! {}'.format(problem), file=sys.stderr) 20 21 22 def test_yikes(capsys): 23 yikes('Out of coffee!') 24 out, err = capsys.readouterr() 25 assert out == '' 26 assert 'Out of coffee!' in err
pytest 通常会抓取测试用例及被测试代码的输出。仅当全部测试会话执行结束后,抓取到的输出才会随着失败的测试显示出来。--s 参数可以关闭这个功能,在测试仍在运行期间就把输出直接发送到 stdout。
通常这很方便,但是有时你可能又需要其中的部分信息。此时可以使用 capsys,capsys.disabled() 可以临时让输出绕过默认的输出捕获机制。
1 def test_capsys_disabled(capsys): 2 with capsys.disabled(): 3 print('\nalways print this') 4 print('normal print, usually captured')
4.5 使用 monkeypatch
monkey patch 可以在运行期间对类或模块进行动态修改。在测试中,monkey patch 常用于替换被测试代码的部分运行环境,或者将输入依赖或输出依赖替换成更容易测试的对象或函数。
pytest 内置的 monkeypatch 允许你在单一测试的环境里做这些事情。测试结束后,无论结果是通过还是失败,代码都会复原(所有修改都会撤销)。
monkeypatch 提供以下函数:
- setattr(target, name, value=<notset>, raising=True):设置一个属性
- delattr(target, name=<notset>, raising=True):删除一个属性
- setitem(dic, name, value):设置字典中的一条记录
- delitem(dic, name, raising=True):删除字典中的一条记录
- setenv(name, value, prepend=None):设置一个环境变量
- delenv(name, raising=True):删除一个环境变量
- syspath_prepend(path):将路径path加入sys.path并放在最前,sys.path是Python导入的系统路径列表
- chdir(path):改变当前的工作目录
raising 参数用于指示 pytest 是否在记录不存在时抛出异常。setenv()函数里的 prepend 参数可以是一个字符,如果这样设置的话,那么环境变量的值就是 value+prepend+<old value>。
1 #----cheese.py 2 3 def read_cheese_preferences(): 4 full_path = os.path.expanduser('~/.cheese.json') 5 with open(full_path, 'r') as f: 6 prefs = json.load(f) 7 return prefs 8 9 10 def write_cheese_preferences(prefs): 11 full_path = os.path.expanduser('~/.cheese.json') 12 with open(full_path, 'w') as f: 13 json.dump(prefs, f, indent=4) 14 15 16 def write_default_cheese_preferences(): 17 write_cheese_preferences(_default_prefs) 18 19 20 _default_prefs = { 21 'slicing': ['manchego', 'sharp cheddar'], 22 'spreadable': ['Saint Andre', 'camembert', 23 'bucheron', 'goat', 'humbolt fog', 'cambozola'], 24 'salads': ['crumbled feta'] 25 }
1 #----test_cheese.py 2 3 def test_def_prefs_change_defaults(tmpdir, monkeypatch): 4 # write the file once 5 fake_home_dir = tmpdir.mkdir('home') 6 monkeypatch.setattr(cheese.os.path, 'expanduser', 7 (lambda x: x.replace('~', str(fake_home_dir)))) 8 cheese.write_default_cheese_preferences() 9 defaults_before = copy.deepcopy(cheese._default_prefs) 10 11 # change the defaults 12 monkeypatch.setitem(cheese._default_prefs, 'slicing', ['provolone']) 13 monkeypatch.setitem(cheese._default_prefs, 'spreadable', ['brie']) 14 monkeypatch.setitem(cheese._default_prefs, 'salads', ['pepper jack']) 15 defaults_modified = cheese._default_prefs 16 17 # write it again with modified defaults 18 cheese.write_default_cheese_preferences() 19 20 # read, and check 21 actual = cheese.read_cheese_preferences() 22 assert defaults_modified == actual 23 assert defaults_modified != defaults_before
4.6 使用 doctest_namespace
doctest 模块是 Python 标准库的一部分,借助它,可以在函数的文档字符串中放入示例代码,并通过测试确保有效。你可以使用 --doctest-modules 标识搜寻并运行 doctest 测试用例。
在构建被标注为 autouse 的 fixture 时,可以使用内置的 doctest_namespace,这能够使 doctest 中的测试用例在运行时识别某些作用于 pytest 命名空间的字符标识,从而增强文档字符串的可读性。
pytest 将每个字符串里的代码看成是不同的测试用例
1 #----unnecessary_math.py 2 3 """ 4 This module defines multiply(a, b) and divide(a, b). 5 6 >>> import unnecessary_math as um 7 8 Here's how you use multiply: 9 10 >>> um.multiply(4, 3) 11 12 12 >>> um.multiply('a', 3) 13 'aaa' 14 15 Here's how you use divide: 16 17 >>> um.divide(10, 5) 18 2.0 19 """ 20 21 22 def multiply(a, b): 23 """ 24 Returns a multiplied by b. 25 26 >>> um.multiply(4, 3) 27 12 28 >>> um.multiply('a', 3) 29 'aaa' 30 """ 31 return a * b 32 33 34 def divide(a, b): 35 """ 36 Returns a divided by b. 37 38 >>> um.divide(10, 5) 39 2.0 40 """ 41 return a / b
1 #----conftest.py 2 3 import pytest 4 import unnecessary_math 5 6 7 @pytest.fixture(autouse=True) 8 def add_um(doctest_namespace): 9 doctest_namespace['um'] = unnecessary_math
4.7 使用 recwarn
内置的 recwarn 可以用来检查待测代码产生的警告信息。
1 def lame_function(): 2 warnings.warn("Please stop using this", DeprecationWarning) 3 # rest of function 4 5 6 def test_lame_function(recwarn): 7 lame_function() 8 assert len(recwarn) == 1 9 w = recwarn.pop() 10 assert w.category == DeprecationWarning 11 assert str(w.message) == 'Please stop using this' 12 13 14 def test_lame_function_2(): 15 with pytest.warns() as warning_list: 16 lame_function() 17 18 assert len(warning_list) == 1 19 w = warning_list.pop() 20 assert w.category == DeprecationWarning 21 assert str(w.message) == 'Please stop using this'
recwarn 的值就像是一个警告信息列表,列表里的每个警告信息都有4个属性 category、message、filename、lineno。
警告信息在测试开始后收集,如果你在意的警告信息出现在测试尾部,则可以在信息收集前使用 recwarn.clear() 清除不需要的内容。
除了 recwarn,pytest 还可以使用 pytest.warns() 来检查警告信息。