简介
unittest官方文档
翻译得不错的文档
详细还是看官方文档,我这里只是对官方文档中的一些笔记简要。
unittest是python标准库里的工具,不需要额外安装,它是一个单元测试框架与pytest的作用是一样的,两者是可以互换。
单元测试框架的作用
- 发现测试用例
- 执行测试用例
- 判断测试结果
- 生成测试报告
unittest五个重要组件
- TestCase测试用例:最小单元,业务逻辑
- TestSuite测试套件:一组测试用例的集合,或者测试套件的集合。
- TestFixtrue测试夹具:执行测试用例之前和之后的操作
- unittest:
- setUp/tearDown 在测试用例之前/后执行,每个用例都会执行
- setUpClass/tearDownClass 在测试类之前/后执行,同一个类,多个用例只执行一次,注意类要加装饰器
- setUpModule/tearDownModule 在测试模块之前和之后执行
- pytest:
- setup/teardown 全小写
- setup_class/teardown_class
- setup_module/teardown_class
- unittest:
TestLoader测试加载器:加载测试用例
TestRunner测试运行器:运行指定的测试用例。
运行方式
# test_module1.py
import unittest
class TestClass(unittest.TestCase):
def setUp(self):
print('setUp')
def tearDown(self):
print('tearDown')
def test_method(self):
print('test_1')
@unittest.skip("skip test_2")
def test_2(self):
print('test_2')
def suite():
suite=unittest.TestSuite()
suite.addTest(TestClass('test_method'))
suite.addTest(TestClass('test_2'))
return suite
if __name__ == '__main__':
runner=unittest.TextTestRunner()
runner.run(suite())
- 命令行
python -m unittest test_module1 test_module2
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method
python -m unittest -k test_module1.Test*
-k 使用通配符
为什么用命令行?因为当测试用例很多的时候,我们可以通过测试平台,指定要运行哪个用例,而不需要去修改代码。 - 通过测试套件来运行
- AddTest
# 创建一个测试套件
suite=unittest.TestSuite()
suite.addTest(TestClass('test_method'))
unittest.main(defaultTest='suite')
- AddTests
suite=unittest.TestSuite()
suite.addTests(TestClass('test_method'),TestClass('test_2')
unittest.main(defaultTest='suite')
- 加载一个目录下的测试用例
if __name__ == '__main__':
suite = unittest.defaultTestLoader.discover('./__path__', pattern = '*.py')
unittest.main(defaultTest='suite')
说说unittest.main()
def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, warnings=None, *, tb_locals=False):
代码了后一行,main = TestProgram
,也就是main是TestProgram类的一个实例,当这个时候就会实例化地执行init函数。
main()里有很多参数
module='main' : 要放在if name == 'main' 是入口函数。
defaultTest=None:默认为全部,可以指定运行某一个。
argv: 可以传递参数进去
testRunner: 测试运行器
testLoader: 测试加载器,使用的是默认的测试使用加载器
exit=True: 是否在测试程序完成之后关闭程序。
verbosity=1:显示信息的详细程度
-
<=0
只显示用例 的总数和全局的执行结果 -
1
默认值,显示用例总数和全局结果,并且对每个用例的结果有个标。
.成功
F失败
E错误
S用例跳过 -
>=2
显示用例总数和全局结果,并输出每个用例的详解的结果
failfast=None:是否在测试失败时终止测试
catchbreak=None:
buffer=None:
warnings=None:
tb_locals=False:
在init函数最后self.runTests()
运行了这个函数。跳到246行。
if self.testRunner is None:
self.testRunner = runner.TextTestRunner
# 和我们平时写的
if __name__ == '__main__':
runner=unittest.TextTestRunner()
runner.run(suite())
回顾一下unittest单元测试框架的五个重要部分
- TestCase测试用例:最小单元,业务逻辑
- TestSuite测试套件:一组测试用例的集合,或者测试套件的集合。
测试用例或套件都在此就绪
# self.testNames 加载了我们所需要的用例并赋给了self.test,
def createTests(self, from_discovery=False, Loader=None):
if self.testNamePatterns:
self.testLoader.testNamePatterns = self.testNamePatterns
if from_discovery:
loader = self.testLoader if Loader is None else Loader()
self.test = loader.discover(self.start, self.pattern, self.top)
elif self.testNames is None:
self.test = self.testLoader.loadTestsFromModule(self.module)
else:
self.test = self.testLoader.loadTestsFromNames(self.testNames,
self.module)
# 250行
self.testRunner = runner.TextTestRunner
# 271行
self.result = testRunner.run(self.test)
- TestFixtrue测试夹具:执行测试用例之前和之后的操作
- TestLoader测试加载器:加载测试用例
def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, warnings=None, *, tb_locals=False):
- TestRunner测试运行器:运行指定的测试用例。
if self.testRunner is None:
self.testRunner = runner.TextTestRunner
总结:
先从main函数开始,它是一个类,而这个类通过TestProgram实例化,在实例化时就会运行init初始化函数,对每个参数进行初始化,关键的是五个部分(用例、测试运行器,测试加载器,实际最常见的是三个),通过我们的模块和测试名称来加载出来。所以从原理来说,只是执行了两句代码,其它都是判断。
# test_case/a.py
import unittest
class TestAll(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
print('setUpClass')
# 测试用例
def test_quickly_process(self):
print('test_quickly_process')
@classmethod
def tearDownClass(cls) -> None:
print('tearDownClass')
if __name__ == '__main__':
unittest.main()
# test_case/b.py
import unittest
class TestModel(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
print('setUpClass')
# 测试用例
def test_ceiling_model(self):
print('test_ceiling_model')
def test_cermic_model(self):
print('test_cermic_model')
def test_door_model(self):
print('test_door_model')
def test_products_model(self):
print('test_products_model')
@classmethod
def tearDownClass(cls) -> None:
print('tearDownClass')
if __name__ == '__main__':
unittest.main()
# test.py
import unittest
if __name__ == "__main__":
# 就这两句
# 测试加载器,加载我们用例里的所有*.py文件
suite = unittest.defaultTestLoader.discover('./test_case', pattern='*.py')
# 测试运行器
unittest.TextTestRunner().run(suite)
# 为什么main()会报错?因为main中deaultTest为空时实际上只会执行当前文件的用例,但是我们的文件现时是空的,没有任何用例所以报错,如果指定了deaultTest='suite'后,他就知道从这个加载器里找这些测试用例了。
# unitest.main(deaultTest='suite')
运行结果
测试夹具
测试夹具(测试脚手架、固件、钩子函数,前后置等)TextFixtrue
- TestFixtrue测试夹具:执行测试用例之前和之后的操作
- unittest:
- setUp/tearDown 在测试用例之前/后执行,每个用例都会执行
- setUpClass/tearDownClass 在测试类之前/后执行,同一个类,多个用例只执行一次,注意类要加装饰器
- setUpModule/tearDownModule 在测试模块之前和之后执行
- pytest:
- setup/teardown 全小写
- setup_class/teardown_class
- setup_module/teardown_class
- unittest:
# ./test_case/b.py
import unittest
class TestModel(unittest.TestCase):
# 类的装饰器不能缺,缺了就报错。测试类之前的准备工作:连接数据库,创建日志对象等
@classmethod
def setUpClass(cls) -> None:
print('setUpClass,每个测试类运行前运行')
@classmethod
def tearDownClass(cls) -> None:
print('tearDownClass,每个测试类运行后运行')
# 测试用例 前的准备工作:可能是打开浏览器,加载网页,登陆、选择好场景等等。
def setUp(self) -> None:
print('setUp每个用例启动前运行')
def tearDown(self) -> None:
print('setUp每个用例结束后前运行')
# 测试用例
def test_ceiling_model(self):
print('test_ceiling_model')
def test_cermic_model(self):
print('test_cermic_model')
def test_door_model(self):
print('test_door_model')
def test_products_model(self):
print('test_products_model')
if __name__ == '__main__':
unittest.main()
夹具的封装
那么如何我们多个用例存在相同的setUp/tearDown setUpClass/tearDownClass那怎么办?我们可以进行夹具封装
# common/unit.py
import unittest
class YfUnit(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
print('setUpClass,每个测试类运行前运行')
@classmethod
def tearDownClass(cls) -> None:
print('tearDownClass,每个测试类运行后运行')
def setUp(self) -> None:
print('setUp每个用例启动前运行')
def tearDown(self) -> None:
print('setUp每个用例结束后前运行')
那么两个test_case可以继承这个YfUnit,修改为,仅用于写用例
#test_case/a.py
from common.unit import YfUnit
class TestModel(YfUnit):
# 测试用例
def test_ceiling_model(self):
print('test_ceiling_model')
def test_cermic_model(self):
print('test_cermic_model')
def test_door_model(self):
print('test_door_model')
def test_products_model(self):
print('test_products_model')
# test_case/b.py
from common.unit import YfUnit
class TestAll(YfUnit):
# 测试用例
def test_quickly_process(self):
print('test_quickly_process')
# all.py
import unittest
if __name__ == "__main__":
# 测试加载器,加载我们用例里的所有*.py文件
suite = unittest.defaultTestLoader.discover('test_case', pattern='*.py')
# 测试运行器
unittest.TextTestRunner().run(suite)
关于用例的添加
Test Suite
通过addTest或addTests把要测试用例添加到套件里
addTest
单独添加测试用例,内容为:类名(“方法名”);
Test2是要测试的类名,test_one是要执行的测试方法
执行其余的方法直接依照添加
suite.addTest(Test2("test_two"))
suite.addTest(Test2("test_one"))
addTests
是将需要执行的测试用例放到一个list后,再进行add,addTests 格式为:addTests(用例list名称);
tests = [Test2("test_two"), Test2("test_one")]
suite.addTests(tests)
这种方式控制粒度到方法,但是如何一个测试类里,有好多个方法呢?
Test Loder
TestLoadder
经常用来从类和模块中提取创建测试套件。一般情况下TestLoader不需要实例化,unittest内部提供了一个实例可以当做unittest.defaultTestLoader使用。无论使用子类还是实例,都允许自定义一些配置属性。
loadTestsFrom*()
方法从各个地方寻找testcase,创建实例,然后addTestSuite,再返回一个TestSuite实例
defaultTestLoader()
与 TestLoader()
功能差不多,复用原有实例
- unittest.TestLoader().loadTestsFromTestCase(测试类名(方法名))
从TestCase派生类testCaseClass中加载所有测试用例并返回测试套件。
getTestCaseNames()会从每一个方法中创建Test Case实例,这些方法默认都是test开头命名。如果getTestCaseNames()没有返回任何方法,但是runTest()方法被实现了,那么一个test case就会被创建以替代该方法。
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all test cases contained in testCaseClass"""
if issubclass(testCaseClass, suite.TestSuite):
raise TypeError("Test cases should not be derived from "
"TestSuite. Maybe you meant to derive from "
"TestCase?")
testCaseNames = self.getTestCaseNames(testCaseClass)
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest']
loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
return loaded_suite
- unittest.TestLoader().loadTestsFromModule(测试模块名)
目测是python 3.5后不再兼容,可以使用loadTestsFromTestCase(测试类名)
代替,也就是不填方法名
从模块中加载所有测试用例,返回一个测试套件。该方法会从module中查找TestCase的派生类并为这些类里面的测试方法创建实例。
注意:当使用TestCase派生类时可以共享test fixtures和一些辅助方法。该方法不支持基类中的测试方法。但是这么做是有好处的,比如当子类的fixtures不一样时。
如果一个模块提供了load_tests方法,它会在加载测试时被调用。这样可以自定义模块的测试加载方式。这就是load_tests协议。pattern作为第三个参数被传递给load_tests。
# XXX After Python 3.5, remove backward compatibility hacks for
# use_load_tests deprecation via *args and **kws. See issue 16662.
def loadTestsFromModule(self, module, *args, pattern=None, **kws):
"""Return a suite of all test cases contained in the given module"""
# This method used to take an undocumented and unofficial
# use_load_tests argument. For backward compatibility, we still
# accept the argument (which can also be the first position) but we
# ignore it and issue a deprecation warning if it's present.
if len(args) > 0 or 'use_load_tests' in kws:
warnings.warn('use_load_tests is deprecated and ignored',
DeprecationWarning)
kws.pop('use_load_tests', None)
if len(args) > 1:
# Complain about the number of arguments, but don't forget the
# required `module` argument.
complaint = len(args) + 1
raise TypeError('loadTestsFromModule() takes 1 positional argument but {} were given'.format(complaint))
if len(kws) != 0:
# Since the keyword arguments are unsorted (see PEP 468), just
# pick the alphabetically sorted first argument to complain about,
# if multiple were given. At least the error message will be
# predictable.
complaint = sorted(kws)[0]
raise TypeError("loadTestsFromModule() got an unexpected keyword argument '{}'".format(complaint))
tests = []
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, type) and issubclass(obj, case.TestCase):
tests.append(self.loadTestsFromTestCase(obj))
load_tests = getattr(module, 'load_tests', None)
tests = self.suiteClass(tests)
if load_tests is not None:
try:
return load_tests(self, tests, pattern)
except Exception as e:
error_case, error_message = _make_failed_load_tests(
module.__name__, e, self.suiteClass)
self.errors.append(error_message)
return error_case
return tests
- unittest.TestLoader().loadTestsFromName(方法名,类名)
从一个字符串中加载测试并返回测试套件。
字符串说明符name是一个虚名(测试方法名的字符串格式),可以适用于模块、测试类、test case类中的测试方法、TestSuite实例或者一个返回TestCase或TestSuite的可调用的对象。并且这样的话,在一个测试用例类中的方法会被当做‘测试用例类中给的测试方法’而不是‘可调用对象’。
比如,你有一个SampleTests模块,里面有一个TestCase的派生类SampleTestCase,类中有3个方法(test_one(),test_two(),test_three()),使用说明符“SampleTests.SampleTestCase”就会返回一个测试套件,里面包含所有3个测试方法。如果使用“SampleTests.SampleTestCase.test_two”就会返回一个测试套件,里面只包含test_two()测试方法。如果说明符中包含的模块或包没有事先导入,那么在使用时会被顺带导入。
def loadTestsFromName(self, name, module=None):
"""Return a suite of all test cases given a string specifier.
The name may resolve either to a module, a test case class, a
test method within a test case class, or a callable object which
returns a TestCase or TestSuite instance.
The method optionally resolves the names relative to a given module.
"""
parts = name.split('.')
error_case, error_message = None, None
if module is None:
parts_copy = parts[:]
while parts_copy:
try:
module_name = '.'.join(parts_copy)
module = __import__(module_name)
break
except ImportError:
next_attribute = parts_copy.pop()
# Last error so we can give it to the user if needed.
error_case, error_message = _make_failed_import_test(
next_attribute, self.suiteClass)
if not parts_copy:
# Even the top level import failed: report that error.
self.errors.append(error_message)
return error_case
parts = parts[1:]
obj = module
for part in parts:
try:
parent, obj = obj, getattr(obj, part)
except AttributeError as e:
# We can't traverse some part of the name.
if (getattr(obj, '__path__', None) is not None
and error_case is not None):
# This is a package (no __path__ per importlib docs), and we
# encountered an error importing something. We cannot tell
# the difference between package.WrongNameTestClass and
# package.wrong_module_name so we just report the
# ImportError - it is more informative.
self.errors.append(error_message)
return error_case
else:
# Otherwise, we signal that an AttributeError has occurred.
error_case, error_message = _make_failed_test(
part, e, self.suiteClass,
'Failed to access attribute:\n%s' % (
traceback.format_exc(),))
self.errors.append(error_message)
return error_case
if isinstance(obj, types.ModuleType):
return self.loadTestsFromModule(obj)
elif isinstance(obj, type) and issubclass(obj, case.TestCase):
return self.loadTestsFromTestCase(obj)
elif (isinstance(obj, types.FunctionType) and
isinstance(parent, type) and
issubclass(parent, case.TestCase)):
name = parts[-1]
inst = parent(name)
# static methods follow a different path
if not isinstance(getattr(inst, name), types.FunctionType):
return self.suiteClass([inst])
elif isinstance(obj, suite.TestSuite):
return obj
if callable(obj):
test = obj()
if isinstance(test, suite.TestSuite):
return test
elif isinstance(test, case.TestCase):
return self.suiteClass([test])
else:
raise TypeError("calling %s returned %s, not a test" %
(obj, test))
else:
raise TypeError("don't know how to make test from: %s" % obj)
- unittest.TestLoader().loadTestsFromNames(多个方法,多个类)
使用方法和loadTestsFromName(name,module=None)一样,一个列表推导式,创建了一个suites列表
,不同的是它可以接收一个说明符列表而不是一个,返回一个测试套件,包含所有说明符中的所有测试用例。
def loadTestsFromNames(self, names, module=None):
"""Return a suite of all test cases found using the given sequence
of string specifiers. See 'loadTestsFromName()'.
"""
suites = [self.loadTestsFromName(name, module) for name in names]
return self.suiteClass(suites)
getTestCaseName(testCaseClass)
返回一个有序的包含在TestCaseClass中的方法名列表。可以看做TestCase的子类。unittest.TestLoader().discover() 探索性测试
从指定的start_dir(起始目录)递归查找所有子目录下的测试模块,并返回一个TestSuite对象。只有符合pattern模式匹配的测试文件才会被加载。模块名称必须有效才能被加载。
顶级项目中的所有模块必须是可导入的。如果start_dir不是顶级路径,那么顶级路径必须单独指出。
如果导入一个模块失败,比如由于语法错误,会被记录为一个单独的错误,然后discovery会继续。如果导入失败是由于设置了SkipTest,那么就会被记录为忽略测试。
当一个包(包含init.py文件的目录)被发现时,这个包会被load_tests方法检查。通过package.load_tests(loader,tests,pattern)方式调用。Test Discovery始终确保包在调用时只进行一次测试检查,即使load_tests方法自己调用了loader.discover。
如果load_tests方法存在,那么discovery就不再递归,load_tests会确保加载完所有的当前包下的测试。
pattern没有被特意存储为loader的属性,这样包就可以自行查找。top_level_dir被存储,所以load_tests就不需要再传参给loader.discover()。
start_dir可以是一个模块名,也可以是一个目录。
#pattern默认是以test开关的py文件
# start_dir开始进行搜索的目录(默认值为当前目录
def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
set_implicit_top = False
if top_level_dir is None and self._top_level_dir is not None:
# make top_level_dir optional if called from load_tests in a package
top_level_dir = self._top_level_dir
elif top_level_dir is None:
set_implicit_top = True
top_level_dir = start_dir
top_level_dir = os.path.abspath(top_level_dir)
if not top_level_dir in sys.path:
# all test modules must be importable from the top level directory
# should we *unconditionally* put the start directory in first
# in sys.path to minimise likelihood of conflicts between installed
# modules and development versions?
sys.path.insert(0, top_level_dir)
self._top_level_dir = top_level_dir
is_not_importable = False
is_namespace = False
tests = []
if os.path.isdir(os.path.abspath(start_dir)):
start_dir = os.path.abspath(start_dir)
if start_dir != top_level_dir:
is_not_importable = not os.path.isfile(os.path.join(start_dir, '__init__.py'))
else:
# support for discovery from dotted module names
try:
__import__(start_dir)
except ImportError:
is_not_importable = True
else:
the_module = sys.modules[start_dir]
top_part = start_dir.split('.')[0]
try:
start_dir = os.path.abspath(
os.path.dirname((the_module.__file__)))
except AttributeError:
# look for namespace packages
try:
spec = the_module.__spec__
except AttributeError:
spec = None
if spec and spec.loader is None:
if spec.submodule_search_locations is not None:
is_namespace = True
for path in the_module.__path__:
if (not set_implicit_top and
not path.startswith(top_level_dir)):
continue
self._top_level_dir = \
(path.split(the_module.__name__
.replace(".", os.path.sep))[0])
tests.extend(self._find_tests(path,
pattern,
namespace=True))
elif the_module.__name__ in sys.builtin_module_names:
# builtin module
raise TypeError('Can not use builtin modules '
'as dotted module names') from None
else:
raise TypeError(
'don\'t know how to discover from {!r}'
.format(the_module)) from None
if set_implicit_top:
if not is_namespace:
self._top_level_dir = \
self._get_directory_containing_module(top_part)
sys.path.remove(top_level_dir)
else:
sys.path.remove(top_level_dir)
if is_not_importable:
raise ImportError('Start directory is not importable: %r' % start_dir)
if not is_namespace:
tests = list(self._find_tests(start_dir, pattern))
return self.suiteClass(tests)
这个可以把该测试类里的所有方法一起执行。
以下这些TestLoader的属性可以在子类和实例中配置:
testMethodPrefix
一种字符串,放在方法的名字前面时,该方法会被当做测试方法。默认一般是‘test’。
在getTestCaseNames()和所有的loadTestsFrom()方法中都有效。
sortTestMethodUsing
在getTestCaseNames()和所有的loadTestFrom()方法中对测试方法进行排序时对测试方法名进行比较的函数。
suiteClass
从一个测试序列中构造出一个测试套件的可调用对象。生成的对象没有方法。默认值是TestSuite类。
对所有的loadTestsFrom*()方法有效。