自动化测试框架之Unittest

Posted HT . WANG

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自动化测试框架之Unittest相关的知识,希望对你有一定的参考价值。

自动化测试之单元测试

单元测试:

通常而言,一个单元测试用例是用于判断某个特定条件或场景下某个特定函数的行为

直观描述:针对一个函数,构造不同输入,验证函数的输出是否符合预期

但需要注意:输入并非都是显性输入,存在隐性输入(读取到的文件或数据库数据)

单元测试的意义:

从质量角度:

  • 测试针对性强,bug更容易暴露
  • 场景构造简单,核心功能验证更充分
  • 保证代码结构良好,具有较高的可测性和可维护性

从效率角度:

  • 测试场景构建快捷,减少调试时间
  • 只针对修改的代码展开测试,减少测试时间
  • 更多bug在项目早期被发现,缩短开发周期

Unitest基本概念

  1. test 测试用例  :针对一个特定场景,特定目的的具体测试用例
  2. testcase  测试类  :可以包含同一个测试对象的多个测试用例(比如一个函数的不同输入)
  3. testsuite  测试集  :可以包含多个测试类的多个测试用例
  4. Assertion  断言   :必须使用断言判断测试结果
  5. testfixture:为测试做统一的初始化和清除工作
  • setUp   在测试类的每个测试用例执行前执行
  • tearDown  在测试类的每个测试用例执行后执行
  • setUpClass  在测试类的第一个测试用例执行前执行
  • tearDownClass 在测试类的最后一个测试用例执行后执行

执行顺序示意如下:(注意两两不需要配对出现

setUpClass
        setUp
                测试用例1
        tearDown
        setUp
                测试用例2
        tearDown
        ……
tearDownClass

示例1: 

#! person.py

class Person(object):

    def __init__(self,name):
        self.name=name

    def get_name(self):
        return self.name
#!persontest.py

import unittest
import person

class PersonTestCase(unittest.TestCase):
    
    def setUp(self):
        #初始化
        self.p1=person.Person('zhang')
        self.p2=person.Person('li')

    def test_get_name(self)#必须test开头才会被认为是测试用例
        self.assertEqual(self.p1.get_name(),'zhang') #断言判断是否一致
        self.assertEqual(self.p1.get_name(),'li')

示例2:

#! company.py

class Company(object):

    def __init__(self,name,boss):
        self.name=name
        self.boss=boss
        self.staffs=set()

    def who is boss(self):
        return boss.get_name()

    def hire(self,person)#雇佣新员工
        '''
            ……
        '''
    
    def fire(self,person)#解雇员工
        '''
            ……
        '''
#! companytest.py

import company
import person
import unittest

class CompanyTestCase(unittest.TestCase):
    @classmethod #必须使用类class装饰器才能使用 setUpClass  
    def setUpClass(self):
        self.boss=person.Person('wang')
        self.acg=company.Company('ACG',self.boss)
    
    def tearDown(self):
        self.acg.staffs.clear()

    def test_who_is_boss(self):#测试用例 老板姓名是否一致
        self.assertEqual(self.acg.who_is_boss(),'wang')

    def test_number_of_staffs_increase_when_hire(self):#测试用例 每雇佣一个人员工数是否加一
        self.acg.hire(person.Person('li'))
        self.assertEqual(len(self.acg.staffs),1)
        self.acg.hire(person.Person('zhang'))
        self.assertEqual(len(self.acg.staffs),2)

    def test_number_of_staffs_notchange_when_hire(self):#测试用例 雇佣相同一个人员工数是否增加
        self.acg.hire(person.Person('li'))
        self.assertEqual(len(self.acg.staffs),1)
        self.acg.hire(person.Person('li'))
        self.assertEqual(len(self.acg.staffs),1)

运行Unittest

if __name__=='__main__':
    unittest.main()

执行输出

·(点)测试正确(测试通过)
F测试失败
E测试异常(给出断言错误)

运行测试集

testsuite  测试集  :可以包含多个测试类的多个测试用例

def MyTestSuite():

    suite=unittest.TestSuite()

    #只针对部分测试类的其中一个测试用例进行测试
    suite.addTest(PersonTestCase('test_get_name'))
    suite.addTest(CompanyTestCase('test_who_is_boss'))

    return suite

if __name__=='__main__':
    unittest.main(defaultTest='MyTestSuite')

单元测试规范

  • 必须使用断言判断结果 unittest.TestCase.assert
    assertEqual  /  assertNotEqual判断相等或不等
    assertTrue  / assertFalse判断boolean(True  False)
    assertRaises判断抛出异常是否符合预期
  • 测试用例需要有自表述能力 ,达到见名知意
  • 测试用例之间相互独立,不应相互依赖,相互调用
  • 一个测试用例只测一个函数 
  • 代码需要保持一致性(非必要不引入 time.time()和random.random())

统计单元测试覆盖率

coverge工具

  • 生成覆盖率统计文件.coverge

替换python test.py arg1 arg2coverge run --branch test.py arg1 arg2

  • 移除部分不需要统计覆盖率文件

coverge report -m --omit=file1,file2

  • 打印覆盖率信息

coverge report -m 获取当前目录下.coverge文件

Stmts总行数
Miss未覆盖行数
Branch总分支行数
BrPart未覆盖分支数
Cover覆盖率
Missing未覆盖具体信息(未覆盖行号信息)

进一步信息可视化(直观便捷的html报告)

coverge html --omit=file1,file2

Mock工具

为什么要使用Mock?

  1. 要测模块A,但它要调用的模块B还未开发完成
  2. 代码中有结果不可预知的代码(随机数 当前时间)
  3. 其他模块的质量不可靠

Mock目的:

  • 构造模块
  • 改变函数逻辑
  • 减少依赖

常见场景:

  • 场景1:永远返回我们想要的返回值 

关键字:return_value

支持python常见数据类型

#! mockdemo01.py

import company
import person
import mock

boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)

boss.get_name=mock.Mock(return_value='li')
print shoeshop.who_is_boss()

boss.get_name=mock.Mock(return_value=12345)
print shoeshop.who_is_boss()

boss.get_name=mock.Mock(return_value=[1,2,'4',5])
print shoeshop.who_is_boss()
  • 场景2:根据调用次数返回想要的结果  每次调用测试返回结果都可以不一致

关键字:side_effect

超出调用次数抛StopIteration异常

#! mockdemo02.py

import company
import person
import mock

boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)

boss.get_name=mock.Mock(side_effect=['li',12345])

print shoeshop.who_is_boss() #li

print shoeshop.who_is_boss() #12345

print shoeshop.who_is_boss() #StopIteration异常  超过设定调用次数
  • 场景3 根据参数返回想要的结果

关键字:side_effect

替换函数逻辑

#! mockdemo03.py

import company
import person
import mock

boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)

def new_logic(arg):
    pairs='a':123,
        True:'abc'
        
    return pairs[arg]

boss.get_name=mock.Mock(side_effect=new_logic)

print boss.get_name(True) #abc
print boss.get_name('a')  #123
  • 场景4 抛出想要的异常或错误

关键字:side_effect

#! mockdemo04.py

import company
import person
import mock

boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)

boss.get_name=mock.Mock(side_effect=KeyError('keyerror'))

print shoeshop.who_is_boss() #抛出异常 keyerror
  • 场景5 限制mock作用域

关键字:with  patch.object

#! mockdemo05.py

import company
import person
import mock

boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)

print shoeshop.who_is_boss() #zhang

with mock.patch.object(person.Person,'get_name',return_value='li'):
    print shoeshop.who_is_boss() #li

print shoeshop.who_is_boss() #zhang
  • 场景6 获取调用信息
called函数是否被调用(Boolean)
call_count函数被调用次数
call_args函数被调用最后一次参数
call_args_list函数被调用参数列表
#! mockdemo06.py

import company
import person
import mock

boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)

boss.get_name=mock.Mock(side_effect=boss.get_name)

print boss.get_name.called  #False
print boss.get_name.call_count  #0
print boss.get_name.call_args  #None
print boss.get_name.call_args_list #[]

print shoeshop.who_is_boss()
print shoeshop.who_is_boss()
print shoeshop.who_is_boss()

print boss.get_name.called  #True
print boss.get_name.call_count  #3
print boss.get_name.call_args  #call()
print boss.get_name.call_args_list #[call(),call(),call()]
  • 场景7 在返回值改变的同时,确保API不会因为mock改变

关键字:create_autospec

#! mockdemo07.py

import company
import person
import mock

boss=person.Person('zhang')
shoeshop=company.Company('shoe shop',boss)

shoeshop.who_is_boss=mock.create_autospec(shoeshop.who_is_boss,return_value='li')

print shoeshop.who_is_boss() #li

print shoeshop.who_is_boss('abc') #抛出异常
  • 场景8 从零构造依赖模块

适用于依赖模块只定义了接口,但尚未开发场景

#! mockdemo08.py

import company

import mock

#company模块中调用的person模块尚未开发 需要构造
boss=mock.Mock()
boss.get_name=mock.Mock(return_value='zhang')

shoeshop=company.Company('shoe shop',boss)

print shoeshop.who_is_boss() #zhang
  • 场景9 替换函数调用链

函数调用链:os.popen(cmd).read().split()

Mock方法:

os.popen=mock.Mock()

os.popen.return_value.read.return value.split.return_value=xx

#! mockdemo09.py

import os
import mock

print os.popen('hostname').read().split() #['host']

os.popen=mock.Mock()

os.popen.return_value.read.return value.split.return_value='mock'

print os.popen('hostname').read().split() #mock

以上是关于自动化测试框架之Unittest的主要内容,如果未能解决你的问题,请参考以下文章

Python接口自动化测试之pytest与unittest区别

软件测试框架之unittest与pytest的对比-

自动化测试框架之UnitTest

单元测试框架之unittest

Python自动化测试之详解Unittest单元测试框架

Python测试框架之unittest和pytest