python之UnittTest模块

一. UnitTest单元测试框架

1.1概述

unittest原名为PyUnit,是由java的JUnit衍生而来。单元测试是对程序中最小的可测试模块(函数)来进行测试;对于单元测试,需要设置预先条件,对比预期结果和实际结果。

unittest有四个重要的面向对象概念:

1)test fixture:这个概念主要处理测试环境的搭建和清理。很多时候我们在进行测试的时候需要搭建合适的环境,例如创建目录、创建数据库等,而在测试完毕后这些环境又不再需要了。test fixture可以帮我们很好的处理这些事情。

2)test case: 既然要进行测试,测试用例当然是最重要的,每一项测试内容都是一个test case。测试用例是指一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。单元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。

3)test suite:我们当然不希望只能一项项的进行测试,最好是将要测试的项目放在一起。test suite相当于test case的集合,当然test suite也能嵌套在test suite中。

4)test runner:顾名思义,这个概念负责执行测试并控制结果输出。

1.2 常用方法总结

<1>unittest的属性如下:
['BaseTestSuite', 'FunctionTestCase', 'SkipTest', 'TestCase', 'TestLoader', 'TestProgram', 'TestResult', 'TestSuite', 'TextTestResult', 'TextTestRunner', '_TextTestResult', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', '__unittest', 'case', 'defaultTestLoader', 'expectedFailure', 'findTestCases', 'getTestCaseNames', 'installHandler', 'loader', 'main', 'makeSuite', 'registerResult', 'removeHandler', 'removeResult', 'result', 'runner', 'signals', 'skip', 'skipIf', 'skipUnless', 'suite', 'util']

unittest类常用属性和方法说明:
1)unittest.TestCase:TestCase类,所有测试用例类继承的基本类。用法:class BaiduTest(unittest.TestCase):
2)unittest.main():使用它可以方便的将一个单元测试模块变为可直接运行的测试脚本,main()方法使用TestLoader类来搜索所有包含在该模块中以“test”命名开头的测试方法,并自动执行他们。执行方法的默认顺序是:根据ASCII码的顺序加载测试用例,数字与字母的顺序为: 0-9,A-Z,a-z。所以以A开头的测试用例方法会优先执行,以a开头会后执行。
3)unittest.TestSuite():unittest框架的TestSuite()类是用来创建测试套件的。
4)unittest.TextTestRunner():unittest框架的TextTestRunner()类,通过该类下面的run()方法来运行suite所组装的测试用例,入参为suite测试套件。
5)unittest.defaultTestLoader():defaultTestLoader()类,通过该类下面的discover()方法可自动根据测试目录start_dir匹配查找测试用例文件(test*.py),并将查找到的测试用例组装到测试套件,因此可以直接通过run()方法执行discover。用法如下:
discover=unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')
6)unittest.skip():装饰器,当运行用例时,有些用例可能不想执行,可用装饰器暂时屏蔽该条测试用例。一种常见的用法:想调试某一个测试用例,而先屏蔽其他用例时就可以用装饰器屏蔽。
@unittest.skip(reason): skip(reason)装饰器:无条件跳过装饰的测试,并说明跳过测试的原因。

@unittest.skipIf(reason): skipIf(condition,reason)装饰器:条件为真时,跳过装饰的测试,并说明跳过测试的原因。

@unittest.skipUnless(reason): skipUnless(condition,reason)装饰器:条件为假时,跳过装饰的测试,并说明跳过测试的原因。

@unittest.expectedFailure(): expectedFailure()测试标记为失败。

<2>TestCase类的属性如下:

['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_addSkip', '_baseAssertEqual', '_classSetupFailed', '_deprecate', '_diffThreshold', '_formatMessage', '_getAssertEqualityFunc', '_truncateMessage', 'addCleanup', 'addTypeEqualityFunc', 'assertAlmostEqual', 'assertAlmostEquals', 'assertDictContainsSubset', 'assertDictEqual', 'assertEqual', 'assertEquals', 'assertFalse', 'assertGreater', 'assertGreaterEqual', 'assertIn', 'assertIs', 'assertIsInstance', 'assertIsNone', 'assertIsNot', 'assertIsNotNone', 'assertItemsEqual', 'assertLess', 'assertLessEqual', 'assertListEqual', 'assertMultiLineEqual', 'assertNotAlmostEqual', 'assertNotAlmostEquals', 'assertNotEqual', 'assertNotEquals', 'assertNotIn', 'assertNotIsInstance', 'assertNotRegexpMatches', 'assertRaises', 'assertRaisesRegexp', 'assertRegexpMatches', 'assertSequenceEqual', 'assertSetEqual', 'assertTrue', 'assertTupleEqual', 'assert_', 'countTestCases', 'debug', 'defaultTestResult', 'doCleanups', 'fail', 'failIf', 'failIfAlmostEqual', 'failIfEqual', 'failUnless', 'failUnlessAlmostEqual', 'failUnlessEqual', 'failUnlessRaises', 'failureException', 'id', 'longMessage', 'maxDiff', 'run', 'setUp', 'setUpClass', 'shortDescription', 'skipTest', 'tearDown', 'tearDownClass']

TestCase类常用属性和方法说明:

1)setUp()方法用于测试用例执行前的初始化工作。如测试用例中需要访问数据库,可以在setUp中建立数据库连接并进行初始化。如测试用例需要登录web,可以先实例化浏览器。在所有的测试方法调用之前调用(自动调用),用来测试fixture,除了AssertionError或SkipTest之外,该方法抛出的异常都视为error,而不是测试不通过。

2)tearDown()方法用于测试用例执行之后的善后工作。如关闭数据库连接。关闭浏览器。tearDown() 清理函数,在所有的测试方法调用之后调用(自动调用),无参数,无返回值。测试方法抛出异常,该方法也正常调用,该方法抛出的异常都视为error,而不是测试不通过。只用setUp()调用成功,该方法才会被调用。没有默认的实现。通过setup 和 tearDown组装一个module成为一个固定的测试装置。注意:如果setup运行抛出错误,测试用例代码不会执行。但是,如果setup执行成功,不管测试用例是否执行成功都会执行teardown。

3)assert*():一些断言方法:在执行测试用例的过程中,最终用例是否执行通过,是通过判断测试得到的实际结果和预期结果是否相等决定的。

4)setUpClass()类初始化方法,在单个类中的所有测试方法调用之前调用,setUpClass作为唯一的参数被调用时,必须使用classmethod()作为装饰器。

5)tearDownClass()类清理方法,在单个类中的所有测试方法调用之后调用,tearDownClass作为唯一的参数被类调用,必须使用classmethod()作为装饰器。

import unittest 
class Test(unittest.TestCase): 
    @classmethod 
    def setUpClass(cls): #这里的cls是当前类的对象 
        cls._connection = createExpensiveConnectionObject()         
    @classmethod 
    def tearDownClass(cls): 
        cls._connection.destroy()     
View Code 

注意: 用setUp与setUpClass区别

setup():每个测试case运行前运行
teardown():每个测试case运行完后执行
setUpClass():必须使用@classmethod 装饰器,所有case运行前只运行一次
tearDownClass():必须使用@classmethod装饰器,所有case运行完后只运行一次

6)run(result=None)运行测试,并返回测试结果(返回值为对象),如果结果被省略或者没有,则创建一个临时结果对象(通过调用defaultTestResult()方法)并使用。

7)skipTest(reason)在测试方法或setUp调用该方法可跳过当前测试

8)debug()以不采集测试结果方式运行测试

9)shortDescription()返回一行描述的测试结果信息

TestCase类提供的常用断言方法总结如下:

assertAlmostEqual(first, second[, places, ...])

适用于小数,place是应最少几位相等布尔值才为1(默认为7),如果在place位以内不同则断言失败。

assertDictContainsSubset(expected, actual[, msg])

检查实际是否超预期

assertDictEqual(d1, d2[, msg])

前后字典是否相同

assertEqual(first, second[, msg])

前后两个数不等,断言失败

assertFalse(expr[, msg])

检查表达式是否为假

assertGreater(a, b[, msg])

和self.assertTrue(a > b)用法一样,但是多了设置条件 .

assertGreaterEqual(a, b[, msg])

和self.assertTrue(a > =b)用法一样,但是多了设置条件 .

assertIn(member, container[, msg])

self.assertTrue(a in b)

assertIs(expr1, expr2[, msg])

assertTrue(a is b)

assertIsInstance(obj, cls[, msg])

Isinstance(a,b)

assertIsNone(obj[, msg])

Obj is none.

assertIsNot(expr1, expr2[, msg])

a is not b.

assertIsNotNone(obj[, msg])

Obj is not none.

assertItemsEqual(expected_seq, actual_seq[, msg])

一个无序的序列特异性的比较。

assertLess(a, b[, msg])

Just like self.assertTrue(a < b), but with a nicer default message.

assertLessEqual(a, b[, msg])

Just like self.assertTrue(a <= b), but with a nicer default message.

assertListEqual(list1, list2[, msg])

List1与list2是否相等.

assertMuitiLineEqual(first, second[, msg])

断言,2个多行字符串是相等的

assertNotAlmostEqual(first, second[, ...])

Fail if the two objects are equal as determined by their difference rounded to the given number of decimal places (default 7) and

comparing to zero, or by comparing that the betweenthe two objects is less than the given delta.

assertNotAlmostEquals(first, second[, ...])

Fail if the two objects are equal as determined by their difference rounded to the given number of decimal places (default 7) and

comparing to zero, or by comparing that the between the two objects is less than the given delta.

assertNotEqual(first, second[, msg])

Fail if the two objects are equal as determined by the ‘==’

assertNotEquals(first, second[, msg])

Fail if the two objects are equal as determined by the ‘==’

assertNotIn(member, container[, msg])

Just like self.assertTrue(a not in b), but with a nicer default message.

assertNotIsInstance(obj, cls[, msg])

Included for symmetry with assertIsInstance.

assertNOtRegexpMatches(text, unexpected_regexp)

如果文本匹配正则表达式,将失败。

assertRaises(excClass[, callableObj])

除非excclass类抛出异常失败

assertRaisesRegexp(expected_exception, ...)

认为在引发异常的情况下消息匹配一个正则表达式。

assertRegexpMatches(text, expected_regexp[, msg])

测试失败,除非文本匹配正则表达式。

assertSequenceEqual(seq1, seq2[, msg, seq_type])

有序序列的相等断言 (like lists and tuples).

assertSetEqual(set1, set2[, msg])

A set-specific equality assertion.

assertTrue(expr[, msg])

Check that the expression is true.

assertTupleEqual(tuple1, tuple2[, msg])

A tuple-specific equality assertion.

assert_(expr[, msg])

Check that the expression is true.


所有的断言方法(除了assertRaises(), assertRaisesRegexp())接受一个msg参数,如果指定的话,它被用作失败的错误消息。
<3>TestSuite类的属性如下:(组织用例时需要用到)

['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_addClassOrModuleLevelException', '_get_previous_module', '_handleClassSetUp', '_handleModuleFixture', '_handleModuleTearDown', '_tearDownPreviousClass', '_tests', 'addTest', 'addTests', 'countTestCases', 'debug', 'run']

TestSuite类常用属性和方法总结:

1)addTest(): addTest()方法是将测试用例添加到测试套件中。

示例:将test_baidu模块下的BaiduTest类下的test_baidu测试用例添加到测试套件。
suite = unittest.TestSuite()
suite.addTest(test_baidu.BaiduTest('test_baidu'))

<4>TextTestRunner的属性如下:(组织用例时需要用到)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_makeResult', 'buffer', 'descriptions', 'failfast', 'resultclass', 'run', 'stream', 'verbosity']

常用方法说明:
run(): run()方法是运行测试套件的测试用例,入参为suite测试套件。
runner = unittest.TextTestRunner()
runner.run(suite)

二. UnitTest的基本使用

2.1 UnitTest的基本使用方法

1)import unittest

2)定义一个继承自unittest.TestCase的测试用例类

3)定义setUp和tearDown,在每个测试用例前后做一些辅助工作。

4)定义测试用例,名字以test开头

5)一个测试用例应该只测试一个方面,测试目的和测试内容应很明确。主要是调用assertEqual、assertRaises等断言方法判断程序执行结果和预期值是否相符。
6)调用unittest.main()启动测试
7)如果测试未通过,会输出相应的错误提示。如果测试全部通过则不显示任何东西,这时可以添加-v参数显示详细信息

 示例:

第一步:先在mathfunc.py中准备待测类:

#构建测试类
class Count(object):
    def add(self,x,y):
        return x+y

    def sub(self,x,y):
        return x-y

    def multi(self,a, b):
        return a * b

    def divide(self,a, b):
        return a / b
mathfunc.py

第二步:在test_mathfunc.py中定义测试类,测试类中添加测试方法

import unittest
from mathfunc import *

#定义测试类,父类为unittest.TestCase。
class TestCount(unittest.TestCase):
    #定义setUp()方法用于测试用例执行前的初始化工作
    def setUp(self):
        print('setUp')
        self.obj = Count()
    #定义多个测试用例,以“test_”开头命名的方法
    def test_add(self):
        self.assertEqual(15, self.obj.add(10, 5))

    def test_sub(self):
        self.assertEqual(5, self.obj.sub(10, 5))

    def test_multi(self):
        self.assertEqual(50, self.obj.multi(10, 5))

    def test_divide(self):
        self.assertEqual(2, self.obj.divide(10, 5))
        self.assertEqual(2.5,self.obj.divide(5, 2))
    # 定义tearDown()方法用于测试用例执行之后的善后工作。
    def tearDown(self):
        print('tearDown')
        self.obj = None
test_mathfunc.py

第三步:在test_suite.py中创建测试用例集,并运行测试用例集

创建测试用例集方法总结:

法1:逐个实例化测试用例,并通过addTest方法逐个添加测试用例到测试用例集

import unittest
from test_mathfunc import *
#(法1)逐个实例化测试用例,并通过addTest方法逐个添加测试用例到测试用例集
#定义测试用例管理函数
#构建测试集示例1:
def get_suite():
    #实例化测试用例
    demo_countadd = TestCount('test_add')
    demo_countsub = TestCount('test_sub')
    demo_countmulti = TestCount('test_multi')
    demo_countdivide = TestCount('test_divide')

    #实例化测试用例集
    suite = unittest.TestSuite()
    #通过addTest方法将测试用例加载到测试用例集
    suite.addTest(demo_countadd)
    suite.addTest(demo_countsub)
    suite.addTest(demo_countmulti)
    suite.addTest(demo_countdivide)
    print(suite)
    return suite
if __name__ == '__main__':
    s =get_suite()
    #统计测试用例条数
    s.countTestCases()
    #实例化TextTestRunner类
    runner = unittest.TextTestRunner()
    #使用run()方法运行测试套件(即运行测试套件中的所有用例)
    runner.run(s)

#运行结果:
#<unittest.suite.TestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_sub>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_divide>]>
#----------------------------------------------------------------------
#Ran 4 tests in 0.000s

#OK
test_suite.py

法2:通过map方法一次实例化多个测试用例,并通过addTests方法同时加载多条用例到测试用例集

import unittest
from test_mathfunc import *
#(法2)通过map方法一次实例化多个测试用例,并通过addTests方法同时加载多条用例到测试用例集
#构建测试集示例2:
def get_suite():
    case_list = ['test_add', 'test_sub','test_multi','test_divide']
    #批量实例化测试用例
    demos = map(TestCount, case_list)
    suite = unittest.TestSuite()
    #通过addTests方法批量加载测试用例
    suite.addTests(demos)
    print(suite)
    return suite
if __name__ == '__main__':
    s =get_suite()
    s.countTestCases()
    runner = unittest.TextTestRunner()
    runner.run(s)
    
#运行结果:
#<unittest.suite.TestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_sub>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_divide>]>
#----------------------------------------------------------------------
#Ran 4 tests in 0.000s
#OK
test_suite.py

法3:继承父类unittest.TestSuite,通过__init__方法直接批量添加测试用例

import unittest
from test_mathfunc import *

#(法3)继承父类unittest.TestSuite,通过__init__方法直接批量添加测试用例
#构建测试集示例3:
class CountTestSuite(unittest.TestSuite):
    def __init__(self):
        case_list = ['test_add','test_sub','test_multi','test_divide']
        unittest.TestSuite.__init__(self,map(TestCount,case_list))

if __name__ == '__main__':
    s  = CountTestSuite()
    print(s)
    s.countTestCases()
    runner = unittest.TextTestRunner()
    runner.run(s)


#运行结果
#<__main__.CountTestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_sub>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_divide>]>
#----------------------------------------------------------------------
#Ran 4 tests in 0.000s

#OK
test_suite.py

 法4:使用unittest.makeSuite()方法自动构建测试用例集,这种方法对于编写测试用例的要求:

(a)测试方法都以规定的命名开头   

(b)使用makeSuite直接生成测试集 

(c)unittest.makeSuite(testcaseClass,prefix='test')    说明:unittest.makeSuite()第一个参数是测试类,第二个参数定义加载的测试用例方法的开头字符

import unittest
from test_mathfunc import *
#(法4)使用unittest.makeSuite()方法自动构建测试用例集,这种方法对于编写测试用例的要求:
#构建测试集示例4:
def get_suite():
  suite = unittest.makeSuite(TestCount,prefix='test')
  return suite


if __name__ == '__main__':
    s  = get_suite()
    print(s)
    s.countTestCases()
    runner = unittest.TextTestRunner()
    runner.run(s)

#运行结果:
#<unittest.suite.TestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_divide>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_sub>]>
#....
#----------------------------------------------------------------------
#Ran 4 tests in 0.000s

#OK
test_suite.py

法5:使用最简单的方式:unitest.main(),这种方法自动检测测试类中所有以test开头的方法;该方法的优点:

(a)自动查找测试用例

(b)自动构建测试集

(c)自动运行测试用例

import unittest
from test_mathfunc import *
#(法5)使用最简单的方式:unitest.main(),这种方法自动检测测试类中所有以test开头的方法;该方法的优点:
#构建测试集示例5:
if __name__ == '__main__':
    unittest.main()
#运行结果:
#----------------------------------------------------------------------
#Ran 4 tests in 0.000s

#OK
test_suite.py

法6:使用unittest.defaultTestLoader.discover构造测试集

用法:discover = unittest.defaultTestLoader.discover(case_dir, pattern="test*.py",top_level_dir=None)

discover方法里面有三个参数:

-case_dir:这个是待执行用例的目录。

-pattern:这个是匹配脚本名称的规则,test*.py意思是匹配test开头的所有脚本。

-top_level_dir:这个是顶层目录的名称,一般默认等于None就行了。

import unittest
from test_case_directory import test_mathfunc
import os.path
import sys
#(法6) 使用unittest.defaultTestLoader.discover构造测试集(简化了先要创建测试套件然后再依次加载测试用例)
#构建测试集示例6:
sys.path.append(os.path.dirname(os.path.realpath(__file__))+ 'test_case_directory')
def get_suite():
    test_dir = os.path.dirname(os.path.realpath(__file__))
    test_dir = os.path.join(test_dir,'test_case_directory')

    suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py',top_level_dir=None)
    print('suite:',suite)
    return suite
if __name__ == '__main__':
    s = get_suite()
    print(s.countTestCases())
    runner = unittest.TextTestRunner()
    runner.run(s)
#运行结果:
#Ran 4 tests in 0.000s
#suite: <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_mathfunc.TestCount testMethod=test_add>, <test_mathfunc.TestCount testMethod=test_divide>, <test_mathfunc.TestCount testMethod=test_multi>, <test_mathfunc.TestCount testMethod=test_sub>]>]>]>

#4
#OK
test_suite.py

2.2 跳过某个测试用例

import unittest
from mathfunc import *

#定义测试类,父类为unittest.TestCase。
class TestCount(unittest.TestCase):
    #定义setUp()方法用于测试用例执行前的初始化工作
    def setUp(self):
        #print('setUp')
        self.obj = Count()
    #定义多个测试用例,以“test_”开头命名的方法
    def test_add(self):
        self.assertEqual(15, self.obj.add(10, 5))

    def test_sub(self):
        self.assertEqual(5, self.obj.sub(10, 5))
    #@unittest.skipIf(Count.version==1,'no test')条件为真时跳过测试用例
    #@unittest.skipUnless(Count.version==1,'no test')#条件为真时不跳过测试用例
    @unittest.expectedFailure #测试结果与预期值不相同,不计入测试失败统计
    def test_multi(self):
        self.assertEqual(50, self.obj.multi(10, 5))
    @unittest.skip('Notest')#无条件跳过该测试用例
    def test_divide(self):
        self.assertEqual(2, self.obj.divide(10, 5))
        self.assertEqual(2.5,self.obj.divide(5, 2))
    # 定义tearDown()方法用于测试用例执行之后的善后工作。

    def tearDown(self):
       # print('tearDown')
        self.obj = None
View Code

2.3 unittest.mock模块

Mock:向测试对象提供一套和测试资源完全相同的接口和方法,不关心具体实现过程,只关心具体结果
参数 说明
name Mock对象的名字
spec Mock对象的属性
return_value Mock对象返回值
mock_calls Mock对象所有调用顺序
call_args Mock对象初始化参数
call_args_list 调用中使用参数
call_count Mock被调用次数
assert_called_with(arg) 检查函数调用参数是否正确
assert_called_once_with(arg) 同上,但是只调用一次
assert_has_calls() 期望调用方法列表
   

 示例:用mock模拟云端客户端接口,然后在客户端功能未实现之前,通过模拟的接口进行用例测试

from unittest import mock
#模拟云端客户端
class CouldClient(object):
    def connect(self):
        pass
    def disconnect(self):
        pass
    def upload(self):
        pass
    def download(self):
        pass
tmock=mock.Mock(CouldClient)
tmock.connect.return_value = 200
tmock.disconnect.return_value = 404
View Code
import unittest
from unittest import mock
from mockcalss import CouldClient
import unittest
class TestCould(unittest.TestCase):
    def setUp(self):
        self.obj = mock.Mock(CouldClient)
    def tearDown(self):
        self.obj = None
    def test_connect(self):
        self.obj.connect.return_value = 200
        self.assertEqual(self.obj.connect(),200)
if __name__ == '__mian__':
    unittest.main()
View Code

 2.4 将测试结果输出到文件

(1)将测试结果输出到txt文件

 将原来的test_suite.py文件做如下改动,便能将测试结果输出到txt格式的文本中

import unittest
from unittest import mock
from test_case_directory import test_mathfunc
import os.path

def get_suite():
    test_dir = os.path.dirname(os.path.realpath(__file__))
    test_dir = os.path.join(test_dir,'test_case_directory')

    suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py',top_level_dir=None)
    print('suite:',suite)
    return suite
if __name__ == '__main__':
    s = get_suite()
    print(s.countTestCases())
    with open('unittestTestReport.txt','a')as f:
        #注意:verbosity参数可以控制输出的错误报告的详细程度,只有3个取值
        #0 (quiet): 只显示执行的用例的总数和全局的执行结果。
        #1 (default): 默认值,显示执行的用例的总数和全局的执行结果,并对每个用例的执行结果(成功T或失败F)有个标注。
        #2 (verbose): 显示执行的用例的总数和全局的执行结果,并输出每个用例的详细的执行结果。
        runner = unittest.TextTestRunner(stream=f,verbosity=2)
        runner.run(s)
test_suite.py

执行此文件,可以看到,在同目录下生成了如下所示的UnittestTextReport.txt,所有的执行报告均输出到了此文件中,这下我们便有了txt格式的测试报告了

test_add (test_mathfunc.TestCount) ... ok
test_divide (test_mathfunc.TestCount) ... ok
test_multi (test_mathfunc.TestCount) ... ok
test_sub (test_mathfunc.TestCount) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK
unittestTextReport.txt

(2)借助HTMLTestRunner生成漂亮的HTML报告

txt格式的文本执行报告过于简陋,这里我们学习一下借助HTMLTestRunner生成HTML报告。首先需要下载HTMLTestRunner.py,并放到当前目录下,或者python目录下的Lib中,就可以导入运行了。

 将原来的test_suite.py文件做如下改动,便能将测试结果输出到HTML格式的文本中

#_*_ coding=utf-8 _*_
import unittest
from unittest import mock
from test_case_directory import test_mathfunc
import HTMLTestRunner
import os.path

def get_suite():
    test_dir = os.path.dirname(os.path.realpath(__file__))
    test_dir = os.path.join(test_dir,'test_case_directory')

    suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py',top_level_dir=None)
    print('suite:',suite)
    return suite
if __name__ == '__main__':
    s = get_suite()
    print(s.countTestCases())
    with open('HTMLReport.html','wb')as f:
        #注意:verbosity参数可以控制输出的错误报告的详细程度,只有3个取值
        #0 (quiet): 只显示执行的用例的总数和全局的执行结果。
        #1 (default): 默认值,显示执行的用例的总数和全局的执行结果,并对每个用例的执行结果(成功T或失败F)有个标注。
        #2 (verbose): 显示执行的用例的总数和全局的执行结果,并输出每个用例的详细的执行结果。
        runner = HTMLTestRunner.HTMLTestRunner(stream=f,title='MathFunc Test Report',description='generated by HTMLTestRunner.',verbosity=2)
        runner.run(s)
test_suite.py

输出 的测试报告如下图:

 (3)增加测试报告的可读性

虽然在我们在测试用例开发时为每个用例添加了注释,但测试报告一般是给非测试人员阅读的,如果能在报告中为每一个测试用例添加说明,那么将会使报告更加易于阅读,

打开我们的测试用例文件,为每一个测试用例(方法)下面添加注释,如下:

import unittest
from conut import *
#定义测试类,父类为unittest.TestCase。
class TestCount(unittest.TestCase):
    #定义setUp()方法用于测试用例执行前的初始化工作
    """测试类:Conut"""
    def setUp(self):
        #print('setUp')
        self.obj = Count()
    #定义多个测试用例,以“test_”开头命名的方法
    def test_add(self):
        """测试加法"""
        self.assertEqual(10, self.obj.add(10, 5))

    def test_sub(self):
        """测试减法"""
        self.assertEqual(5, self.obj.sub(10, 5))

    def test_multi(self):
        """测试乘法"""
        self.assertEqual(50, self.obj.multi(10, 5))

    def test_divide(self):
        """测试除法"""
        self.assertEqual(2, self.obj.divide(10, 5))
        self.assertEqual(2.5,self.obj.divide(5, 2))
    # 定义tearDown()方法用于测试用例执行之后的善后工作。
    def tearDown(self):
        #print('tearDown')
        self.obj = None
View Code

现在生成的测试报告中将会有注释信息,如下:

 

三 . 将测试结果通过邮件发送

使用python3的email模块和smtplib模块可以实现发送邮件的动能。email模块用来生成email,smtplib模块用来发送邮件,接下来看如何在生成测试报告之后,将报告放在邮件附件中并发送给项目组的人

SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。

python的smtplib提供了一种很方便的途径发送电子邮件。它对smtp协议进行了简单的封装。

Python创建 SMTP 对象语法如下:


import smtplib

smtpObj = smtplib.SMTP( [host [, port [, local_hostname]]] )

参数说明:

  • host: SMTP 服务器主机。 你可以指定主机的ip地址或者域名如: runoob.com,这个是可选参数。
  • port: 如果你提供了 host 参数, 你需要指定 SMTP 服务使用的端口号,一般情况下 SMTP 端口号为25。
  • local_hostname: 如果 SMTP 在你的本机上,你只需要指定服务器地址为 localhost 即可。
Python SMTP 对象使用 sendmail 方法发送邮件,语法如下:
SMTP.sendmail(from_addr, to_addrs, msg[, mail_options, rcpt_options])

参数说明:

  • from_addr: 邮件发送者地址。
  • to_addrs: 字符串列表,邮件发送地址。
  • msg: 发送消息

这里要注意一下第三个参数,msg 是字符串,表示邮件。我们知道邮件一般由标题,发信人,收件人,邮件内容,附件等构成,发送邮件的时候,要注意 msg 的格式。这个格式就是 smtp 协议中定义的格式。


以下执行实例需要你本机已安装了支持 SMTP 的服务,如:sendmail。

以下是一个使用 Python 发送邮件简单的实例:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
import smtplib
from email.mime.text import MIMEText
from email.header import Header
 
sender = 'from@runoob.com'
receivers = ['429240967@qq.com']  # 接收邮件,可设置为你的QQ邮箱或者其他邮箱
 
# 三个参数:第一个为文本内容,第二个 plain 设置文本格式,第三个 utf-8 设置编码
message = MIMEText('Python 邮件发送测试...', 'plain', 'utf-8')
message['From'] = Header("菜鸟教程", 'utf-8')   # 发送者
message['To'] =  Header("测试", 'utf-8')        # 接收者
 
subject = 'Python SMTP 邮件测试'
message['Subject'] = Header(subject, 'utf-8')
 
 
try:
    smtpObj = smtplib.SMTP('localhost')
    smtpObj.sendmail(sender, receivers, message.as_string())
    print "邮件发送成功"
except smtplib.SMTPException:
    print "Error: 无法发送邮件"
View Code

 如果我们本机没有 sendmail 访问,也可以使用其他邮件服务商的 SMTP 访问(QQ、网易、Google等)。

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

from email.utils import formataddr
import os.path
from constant.constant import REPORT_PATH
from utils.common.log import logger

# 使用第三方邮件服务商的SMTP发送邮件
#QQ 邮箱 SMTP 服务器地址:smtp.qq.com,ssl 端口:465。
mail_host = "smtp.qq.com"  # 设置服务器
sender = "xxxxxxxx@qq.com"  # 用户名
password = "xxxxxxxxxxxx"  # QQ邮箱的SMTP授权码
receivers = ['xxxxxxxxx@qq.com','xxxxxxxxxxx@163.com']  # 接收邮件,可设置为你的QQ邮箱或者其他邮箱
file = os.path.join(REPORT_PATH, 'ExampleReport.html')#测试报告地址

def mail():
    # 创建一个带附件的实例
    message = MIMEMultipart()
    # 括号里的对应发件人邮箱昵称、发件人邮箱账号
    message['From'] = formataddr(['发件人姓名',sender])
    logger.info('发件人邮箱:%s' % sender)
    # 括号里的对应收件人邮箱昵称、收件人邮箱账号
    #单个收件人: message['To'] = formataddr(['收件人姓名',sender])
    #多个收件人:
    message['To'] = ';'.join(receivers)
    logger.info('收件人邮箱:%s' % receivers)
    # 邮件的主题,也可以说是标题
    message['Subject'] = 'Python SMTP 邮件测试'
    # 邮件正文内容
    message.attach(MIMEText('Python 邮件发送测试...', 'plain', 'utf-8'))
    # 构造附件1
    att1 = MIMEText(open(file, 'rb').read(), 'base64', 'utf-8')
    logger.info('读取附件')
    att1["Content-Type"] = 'text/html'
    # filename是附件名,附件名称为中文时的写法
    #att1.add_header("Content-Disposition", "attachment", filename=("gbk", "", "xxx接口自动化测试报告.html"))
    # 附件名称非中文时的写法
    att1["Content-Disposition"] = 'attachment; filename="ExampleReport.html")'
    #添加附件
    message.attach(att1)
    logger.info('添加附件')
    try:
    # 发件人邮箱中的SMTP服务器,一般端口是25
        server = smtplib.SMTP_SSL(mail_host, 465)
        logger.info('连接QQ邮箱smtp服务')
    # 括号中对应的是发件人邮箱账号、邮箱密码
        server.login(sender, password)
        logger.info('连接成功')
    # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
        server.sendmail(sender, receivers, message.as_string())
    # 关闭连接
        server.quit()
        logger.info("邮件发送成功")
    except Exception:
        logger.error("邮件发送失败", exc_info=1)
View Code

成功发送邮件后,接受邮箱的信息显示如下:

 

 将上面发送邮件的过程抽象成一个类:

class Email:
    def __init__(self, server, sender, password, receiver, title, message=None, path=None):
        """初始化Email

        :param title: 邮件标题,必填。
        :param message: 邮件正文,非必填。
        :param path: 附件路径,可传入list(多附件)或str(单个附件),非必填。
        :param server: smtp服务器,必填。
        :param sender: 发件人,必填。
        :param password: 发件人SMTP授权码,必填。
        :param receiver: 收件人,多收件人用“;”隔开,必填。
        """
        self.title = title
        self.message = message
        self.files = path
        self.msg = MIMEMultipart('related')
        self.server = server
        self.sender = sender
        self.receiver = receiver
        self.password = password

    def _attach_file(self, att_file):
        """将单个文件添加到附件列表中"""
        att = MIMEText(open('%s' % att_file, 'rb').read(), 'plain', 'utf-8')
        att["Content-Type"] = 'application/octet-stream'
        file_name = re.split(r'[\|/]', att_file)
        att["Content-Disposition"] = 'attachment; filename="%s"' % file_name[-1]
        self.msg.attach(att)
        logger.info('attach file {}'.format(att_file))

    def send(self):
        self.msg['Subject'] = self.title
        self.msg['From'] = self.sender
        self.msg['To'] = self.receiver

        # 邮件正文
        if self.message:
            self.msg.attach(MIMEText(self.message))

        # 添加附件,支持多个附件(传入list),或者单个附件(传入str)
        if self.files:
            if isinstance(self.files, list):
                for f in self.files:
                    self._attach_file(f)
            elif isinstance(self.files, str):
                self._attach_file(self.files)

        # 连接服务器并发送
        try:
            smtp_server = smtplib.SMTP(self.server)  # 连接sever
        except (gaierror and error) as e:
            logger.exception('发送邮件失败,无法连接到SMTP服务器,检查网络以及SMTP服务器. %s', e)
        else:
            try:
                smtp_server.login(self.sender, self.password)  # 登录
            except smtplib.SMTPAuthenticationError as e:
                logger.exception('用户名密码验证失败!%s', e)
            else:
                smtp_server.sendmail(self.sender, self.receiver.split(';'), self.msg.as_string())  # 发送邮件
            finally:
                smtp_server.quit()  # 断开连接
                logger.info('发送邮件"{0}"成功! 收件人:{1}。如果没有收到邮件,请检查垃圾箱,'
                            '同时检查收件人地址是否正确'.format(self.title, self.receiver))


if __name__ == '__main__':
    # 使用第三方邮件服务商的SMTP发送邮件
    #QQ 邮箱 SMTP 服务器地址:smtp.qq.com,ssl 端口:465。
    mail_host = "smtp.qq.com"  # 设置服务器
    sender = "xxxxxxxxx@qq.com"  # 用户名
    password = "xxxxxxxxxxxxxxxxx"  # QQ邮箱的SMTP授权码
    receivers = ['xxxxxxxxx@qq.com','xxxxxxxxxxx@163.com']  # 接收邮件,可设置为你的QQ邮箱或者其他邮箱
    file = os.path.join(REPORT_PATH, 'ExampleReport.html')#测试报告地址
    e = Email(title='测试报告',
              message='这是今天的测试报告,请查收!',
              receiver=';'.join(receivers),
              server=mail_host,
              sender=sender,
              password=password,
              path=file
              )
    e.send()
View Code
 



















>>>>>>>待续

原文地址:https://www.cnblogs.com/wuxunyan/p/9397248.html