最近,我一直在学习用pytest进行单元测试,试图找出什么是和什么是不值得测试的。我对它非常陌生,所以我认为复习是一个很好的方法来了解我留下的漏洞和我正在犯的错误。
因此,我主要感兴趣的是正在被审查的测试函数,尽管我也对正在测试的函数进行反馈。
tools.py
函数只是我在脚本中经常需要的常见函数,因此为了方便起见,我想在库中收集它们。提供循环运行距离反馈的进度条、确保文件夹存在的函数和快速连接函数。
注:猎枪是我经常参加的第三方服务。猎枪对象只是一个用于调用查询和对外部数据库进行更新的工具。出于这些目的,您只需要知道它是一个需要进行身份验证的对象。
我试着把我的测试集中在那些可能会被改变打破的事情上,或者在没有立即被注意到的情况下出错。下面是剧本:
import pytest
import os
from time import sleep
from tools import get_shotgun, create_folder, ProgressBar
@pytest.fixture(scope='module')
def shotgun():
return get_shotgun()
def test_get_shotgun_returns_value(shotgun):
assert shotgun is not None, "Shotgun object was not returned"
def test_get_shotgun_credentials(shotgun):
shotgun.find_one("Project", [])
def test_create_folder_invalid_path_raises_errors():
with pytest.raises(OSError):
create_folder("\\\\\\")
def test_create_folder_swallow_existing_folder_exception():
# Folder of current file definitely exists.
folder = os.path.dirname(__file__)
create_folder(folder)
def test_create_folder_returns_original_path():
folder = "dummy"
while os.path.isdir(folder):
folder = folder + '_'
assert folder == create_folder(folder), "Original path was not returned"
try:
os.rmdir(folder)
except OSError:
print("Couldn't delete temporary test folder.")
raise
def test_ProgressBar_validate_input():
with pytest.raises(ValueError):
ProgressBar("zero")
with pytest.raises(ValueError):
ProgressBar(None)
with pytest.raises(ValueError):
ProgressBar(-1213)
def test_ProgressBar_finishing_above_full():
print('\n')
bar = ProgressBar(100)
for _ in range(16):
sleep(0.1)
bar.iter(12)
def test_ProgressBar_context_manager():
print('\n')
with ProgressBar(10) as bar:
for _ in range(10):
sleep(0.1)
bar.iter(1)
def test_ProgressBar_context_manager_exiting():
print('\n')
with ProgressBar(10) as bar:
for i in range(10):
sleep(0.1)
bar.iter(1)
if i > 6:
break
def test_ProgressBar_context_manager_not_mask_errors():
print('\n')
with pytest.raises(ValueError):
with ProgressBar(10) as bar:
int('')
import errno
import os
import sys
from shotgun_api3 import Shotgun
class ProgressBar(object):
"""A simple class for printing messages about progress.
Takes an int `full`, to indicate the intended total progress to make.
`iter()` increases current progress value and prints the result.
A context manager is also supported, allowing
with ProgressBar(123) as bar
"""
def __init__(self, full):
try:
self.full = int(full)
if self.full < 1:
raise ValueError
except (TypeError, ValueError):
raise ValueError("ProgressBar can only take int values > 0.")
self.progress = 0
self.completed = False
@property
def finished(self):
return self.progress >= self.full
def iter(self, increment=1):
"""Increases progress and prints a message showing current progress.
increment defaults to 1, but can increase by any value.
Having more than `full` progress isn't validated, just counted as the
bar being fully finished.
"""
self.progress += increment
sys.stdout.flush()
if self.finished:
self.finish()
return
string = '=' * int((self.progress * 50.0) / self.full)
percent_progress = int((self.progress * 100.0) / self.full)
message = "\r|{:50}|In progress: {}%".format(string, percent_progress)
sys.stdout.write(message)
def finish(self):
if not self.completed:
sys.stdout.write("\r|{0:50}|COMPLETE\t\t\n".format("=" * 50))
self.completed = True
def __enter__(self):
sys.stdout.flush()
message = "\r|{:50}|Initialising".format(0)
sys.stdout.write(message)
return self
def __exit__(self, *exception_values):
if self.finished:
self.finish()
return
string = '=' * int((self.progress * 50.0) / self.full)
percent_progress = int((self.progress * 100.0) / self.full)
message = "\r|{:50}|Exited at: {}%\t\n".format(string, percent_progress)
sys.stdout.write(message)
def get_shotgun():
SERVER_PATH = "REDACTED"
SCRIPT_NAME = "REDACTED"
SCRIPT_KEY = "REDACTED"
return Shotgun(SERVER_PATH, SCRIPT_NAME, SCRIPT_KEY)
def create_folder(filepath, isfile=False):
"""Creates folder for file or folder at `filepath` if necessary.
`isfile` is a boolean to signal whether the filepath is a file or folder.
"""
if isfile:
folder = os.path.dirname(filepath)
else:
folder = filepath
try:
os.makedirs(folder)
except OSError as e:
if e.errno != errno.EEXIST:
raise e
return filepath
您可以使用py.test test_tools.py
运行测试,-v
和-s
标志非常有用。您可能还需要首先使用pip install pytest
。
发布于 2016-08-10 18:51:39
我经常根据我已经写好的内容重构我的测试,在开始时,我从像您这样的测试开始:
def test_get_shotgun_returns_value(shotgun):
然而,过了一段时间,我几乎总是删除那些琐碎的情况,因为如果没有创建测试对象,所有其他测试都会失败。这个测试没有很大的价值,而且测试通常是随机运行的,所以这是另一个不太有用的论点。
我还会为每个带有目录的测试提供test_fixture,以便“清除”以前运行的临时程序,因此您始终可以确定您有相同的条件
你测试的名字不是最好的-我不知道它们是什么意思。
ProgressBar测试--您总是在测试开始时做同样的事情--提取它。
还有一件事-他们测试什么了吗?我没有安装猎枪,但我认为篡改函数的论点不会有多大作用。
您在ProgressBar测试中没有任何断言--这感觉不对。也许你应该返回字符串并用某种准备好的字符串检查它?
我认为我学到的最重要的事情是--使变量名有意义。ProgressBar(100)和ProgressBar(10)有什么不同吗?如果是这样的话,就用不同的方式称呼他们。0.1是某种特殊的间隔吗?将其赋值给一个变量,并确保读取器。
最后一件事-请不要使用如果在UTs。很难遵循代码逻辑,在单元测试中让ifs变得很痛苦。
发布于 2016-08-11 20:52:47
你的测试可能会被打破一点。考虑一下
def test_ProgressBar_validate_input():
with pytest.raises(ValueError):
ProgressBar("zero")
with pytest.raises(ValueError):
ProgressBar(None)
with pytest.raises(ValueError):
ProgressBar(-1213)
这里真的有三个不同的测试--它不是一个数字的字符串名,它不是非数字的,并且它是一个非负整数。每一个测试都应该是不同的测试;这样,如果一个人失败了,问题的根源就不会有歧义。
然后,您还可以给出更多的描述性名称。最后看起来会是这样
def test_ProgressBar_raises_if_negative():
with pytest.raises(ValueError):
ProgressBar(-1213)
def test_ProgressBar_raises_if_string_name():
with pytest.raises(ValueError):
ProgressBar("zero")
def test_ProgressBar_raises_if_not_convertable():
with pytest.raises(ValueError):
ProgressBar(None)
顺便说一下,我并不认为第二次和第三次测试的价值是不同的--它们似乎基本上是一回事(对它们调用int
失败)。
我也不喜欢你实际上是在为你的单元测试连接散弹枪--在测试中连接到服务、数据库等--这使得它更像是一个集成测试,它可以减缓事情的发展(连接到一个真正的服务需要时间),如果服务是问题,而不是代码,也会导致问题。如果您在另一个应用程序中使用猎枪,并且希望测试该功能,那么它应该在测试期间使用DI并注入模拟服务。
最大的问题是你的测试只是测试猎枪的方法,这大概是库应该已经测试过的。编写自己的测试只会在它们的库未经测试或不稳定的情况下增加价值,在这些情况下,您可能不想使用该库。
https://codereview.stackexchange.com/questions/138327
复制相似问题