1. 认识unittest

什么是单元测试?单元测试负责对最小的软件设计单元(模块)进行验证,它使用软件设计文档中对模块的描述作为指南,对重要的程序分支进行测试以发现模块中的错误。在python语言下有诸多单元测试框架,如doctest、unittest、pytest、nose等,unittest框架(原名PyUnit框架)为Python语言自带的单元测试框架,Python 2.1及其以后的版本已将unittest作为一个标准模块放入Python开发包中,Python 3版本也一样。

1.1 认识单元测试

可能有人会问不用单元测试框架能写单元测试吗?答案是肯定的,单元测试本身就是通过一段代码去验证另一段代码,所以不用单元测试框架也可以写单元测试,下面就通过例子演示不用测试框架的单元测试。

首先创建一个被测试类:

#计算器类
class Count: def __init__(self,a,b):
self.a = int(a)
self.b = int(b) #计算加法
def add(self):
return self.a +self.b

程序非常简单,创建一个Count类用于两个整数的计算,通过__init__()方法对两个数进行初始化,接着创建add()方法返回两个数相加的结果。

根据上面所实现的功能,不用测试框架所编写的单元测试如test1.py。

from test.test2 import Count

#测试两个整数相加
class TestCount:
def test_add(self):
try:
j = Count(2,3)
add = j.add()
assert(add == 5),'Integer addition result error!'
except AssertionError as msg:
print(msg)
else:
print('Test pass') #执行测试类的测试方法
mytest=TestCount()
mytest.test_add()

首先,引入test2文件中的Count类,然后在test_add()方法中调用Count类并传入两个参数2和3,最后调用Count类中的add()方法对两个参数做加法运算,并通过assert()方法判断add()的返回值是否等于5。如果不相等则抛出自定义的“Integer addition result error!”异常信息,如果相等则打印“Test pass”。

运行结果1:

运行结果2:

不难发现这种测试方法存在的问题很多。首先,测试程序的写法没有一定的规范可以遵循,十个程序员完全可能写出十种不同的测试程序来,不统一的代码维护起来会十分麻烦。其次,需要编写大量的辅助代码才能进行单元测试,在test1.py中用于测试的代码甚至比被测试的代码还要多,而且这仅仅是一个测试用例,对一个单元模块来说,只编写一条测试用例显然是不够的。

为了让单元测试代码更容易维护和编写,最好的方式是遵循一定的规范来编写测试用例,这也是单元测试框架诞生的初衷。接下来讲如何通过unittest单元测试框架编写单元测试用例。

from test.test2 import Count
import unittest class TestCount(unittest.TestCase): def setUp(self):
print("test start") def test_add(self):
j = Count(2, 3)
self.assertEqual(j.add(),5) def tearDown(self):
print("test end") if __name__ == '__main__':
unittest.main()

分析上面的代码,首先引入unittest模块,创建TestCount类继承unittest的TestCase类,我们可以将TestCase类看成是特定类进行测试的集合。

setUp()方法用于测试用例执行前的初始化工作,这里只简单打印“test start”信息。tearDown()方法与setUp()方法相呼应,用于测试用例执行之后的善后工作,这里打印“test end”信息。

在test_add()中首先调用Count类并传入要计算的数,通过add()方法得到两数相加的返回值。这里不再使用繁琐的异常处理,而是调用unittest框架所提供assertEqual()方法对add()的返回值进行断言,判断两者是否相等,assertEqual()方法由TestCase类继承而来。

unittest提供了全局的main()方法,使用它可以方便的将一个单元测试模块变成可以直接运行的测试脚本。main()方法使用TestLoader类来搜索所有包含在该模块中以“test”命名开头的测试方法,并自动执行它们。

if __name__ == "__main__":语句说明

在后面的实例中我们会经常使用这个语句,在解释它之前先补充点python知识:

python文件的后缀为.py

.py文件既可以用来直接执行,就像一个小程序一样,也可以用来作为模块被导入

在python中导入模块一般使用的是import。

如果对这个不了解的,可以去找我写的python基础。

顾名思义,if就是如果的意思,在句子开始处加上if,就说明这个句子是一个条件语句。接着是__name__, __name__作为模块的内置属性,简单的说,就是.py文件的调用方式。最后是__mian__,如上所述,.py文件有两种使用方式:作为模块被调用和直接使用,如果它等于“__mian__”就表示是直接使用。

1.2 重要的概念

在unittest的文档中有4个重要的概念:test fixture、test case、test suite和test runner,只有理解了这几个概念才能理解单元测试的基本特征。

1.2.1 Test Case

一个TestCase的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp) 、实现测试过程的代码(run),以及测试后环境的还原(tearDown)。单元测试(unit test)的本质也就在这里,一个测试用例就是一个完整的测试单元,通过运行这个测试单元,可以对某一个功能进行验证。

1.2.2 Test Suite

一个概念的验证往往需要多个测试用例,可以把多个测试用例集合在一起来执行,这就产生了测试套件TestSuite的概念。Test Suite用来组装单个测试用例。可以通过addTest加载TestCase到TestSuite中,从而返回一个TestSuite实例。

1.2.3 Test Runner

测试的执行也是单元测试中非常重要的一个概念,一般单元测试框架中都会提供丰富的执行策略和执行结果。在unittest单元测试框架中,通过TextTestRunner类提供的run()方法来执行test suite/test case。test runner可以使用图形界面、文本界面,或返回一个特殊的值等方式来表示测试执行的结果。

1.2.4 Test Fixture

对一个测试用例环境的搭建和销毁,就是一个fixture,通过覆盖TestCase的setUp()和tearDown()方法来实现。有什么用呢?比如说在这个测试用例中需要访问数据库,那么可以在setUp()通过建立数据库连接来进行初始化,在tearDown()中清除数据库产生的数据,然后关闭连接等。

注意:tearDown的过程很重要,要为下一个test case留下一个干净的环境。

理解这几个概念后,我们再结合例子来学习下吧。

# from test.test2 import Count
import unittest #计算器类
class Count: def __init__(self,a,b):
self.a = int(a)
self.b = int(b) #计算加法
def add(self):
return self.a +self.b class TestCount(unittest.TestCase): def setUp(self):
print("test start") def test_add(self):
j = Count(2, 3)
print("1执行了")
self.assertEqual(j.add(),5) def test_add2(self):
j = Count(5, 3)
print("2执行了")
self.assertEqual(j.add(),8) def test_add3(self):
j = Count(4, 5)
print("3执行了")
self.assertEqual(j.add(), 9) def tearDown(self):
print("test end") if __name__ == '__main__': # 构造测试集
suite = unittest.TestSuite()
suite.addTest(TestCount("test_add2"))
# 执行测试
runner = unittest.TextTestRunner()
runner.run(suite)

在前面的例子的基础上编写了第二个测试用例test_add2()。由于第一条测试用例已经运行通过,因此这次只需运行第二条测试用例。在代码的最后,我们去掉了main()方法,采用构造测试集的方法来加载与运行测试用例,实现了有选择的执行测试用例。(这个时候我发现了一个bug,就是pycharm会执行全部测试用例,所以我把计算机类放到了test.py中,使用cmd来运行,就没有问题了)当然,也可以通过注释的方式注释掉第一条用例,但这种做法会导致页面太多注释,影响观看。。

首先,调用unittest框架的TestSuite()类来创建测试套件,通过它所提供的addTest()方法来添加测试用例test_add2()。接着调用unittest框架的TextTestRunner()类,通过它下面的run()方法来运行suite所组装的测试用例。

运行结果:

从运行结果可以看到,setUp/tearDown作用于测试用例的开始和结束。

1.3 断言方法

在执行用例的过程中,最终用例是否执行通过,是通过判断测试得到的实例结果与预期结果是否相等决定的。unittest框架的TestCase类提供了下面这些方法用于测试结果的判断。

assertEqual(first,second,msg=None):断言第一个参数和第二个参数是否相等,如果不相等则测试失败。msg为可选惨死你,用于定义测试失败时打印的信息。

import unittest

class Test(unittest.TestCase):

    def setUp(self):
print("test start")
number = input("Enter a number:")
self.number = int(number) def test_case(self):
self.assertEqual(self.number,10,msg="Your input is not 10!") def tearDown(self):
print("test end") if __name__ == '__main__': unittest.main()

在setUp()方法中要求用户输入一个数,在test_case()中通过assertEqual()比较输入的数是否等于10,如果不相等则输出msg中定义的提示信息。

运行结果:

从运行结果看到,输入一个20,显然与预期的10不相等,msg所定义的提示信息告诉我们“Your input is not 10!”。

assertNotEqual(first,second,msg=None):与assertEqual相反,它用于断言第一个参数与第二个参数是否不相等,如果相等则测试失败。

assertTrue(expr,msg=None)和assertFalse(expr,msg=None):测试表达式是true或false。

下面来测试判断一个数是否为质数的功能,所谓的质数(又叫素数)是指只能被1和它本身整除的数。

def is_prime(n):
if n <= 1:
return False
for i in range(2,n):
if n % i == 0:
return False
return True

创建is_prinme()函数用于实现对质数的判断。当得到一个数字n后,首先判断它是否小于或等于1,如果小于或等于1,则直接返回False;如果大于1,则对其进行循环判断;若能整除2到其自身之间的任意一个数,则不为质数,返回False,否则返回True。

# from test.test2 import is_prime
import unittest def is_prime(n):
if n <= 1:
return False
for i in range(2,n):
if n % i == 0:
return False
return True class Test(unittest.TestCase): def setUp(self):
print("test start") def test_case(self):
self.assertTrue(is_prime(8),msg="Is not prime!") def tearDown(self):
print("test end") if __name__ == '__main__': unittest.main()

运行结果:

在调用is_prime()函数时分别传不同的值来执行测试用例,在上面的例子中传值为8,显然不是一个质数,所以通过assertTrue的断言得到的结果为False。

assertIn(first,second,msg=None)和assertNotIn(first,second,msg=None):断言第一个参数是否在第二个参数中,反过来讲,第二个参数是否包含第一个参数。

import unittest

class Test(unittest.TestCase):

    def setUp(self):
print("test start") def test_case(self):
a = "hello"
b = "hello world"
self.assertIn(a,b,msg="a is not in b") def tearDown(self):
print("test end") if __name__ == '__main__': unittest.main()

这个很好理解,定义字符串a为“hello”、b为“hello world”。通过assertIn判断b是否包含a,如果不包含则打印msg定义的信息。

assertIs(first,second,msg=None)和assertIsNot(first,second,msg=None):断言第一个参数和第二个参数是否为同一对象。

assertIsNone(expr,msg=None)和assertIsNotNone(expr,msg=None):断言表达式是否为None对象。

assertIsInstance(obj,cls,msg=None)和assertNotIsInstance(obj,cls,msg=None):断言obj是否为cls的一个实例。

断言obj是否为cls的一个实例。

在unittest中还提供了其他检查比较的方法,因为不常用,所以不再一一介绍。大家可以参考Python官方文档unittest章节进行学习。

1.4 组织单元测试用例

当我们增加被测功能和相应的测试用例之后,再来看看unittest单元测试框架是如何扩展和组织新增的测试用例的。

我们同样以前面的计算器为例,为其扩展sub()方法,用来计算两个数相减的结果。

#计算器类
class Count(): def __init__(self,a,b):
self.a = int(a)
self.b = int(b) #计算加法
def add(self):
return self.a + self.b #计算减法
def sub(self):
return self.a - self.b

因为对计算器(calculator)又新增了减法功能(sub),所以需要针对新功能编写测试用例。

from test.test2 import Count
import unittest class TestAdd(unittest.TestCase): def setUp(self):
print("test add start") def test_add1(self):
j = Count(2,3)
print("add1")
self.assertEqual(j.add(),5) def test_add2(self):
j = Count(41,76)
print("add2")
self.assertEqual(j.add(),117) def tearDown(self):
print("test add end") class TestSub(unittest.TestCase): def setUp(self):
print("test sub start") def test_sub1(self):
j = Count(2,3)
print("sub1")
self.assertEqual(j.sub(),-1) def test_sub2(self):
j = Count(71,46)
print("sub2")
self.assertEqual(j.sub(),25) def tearDown(self):
print("test sub end") if __name__ == '__main__': #构造测试集
suite = unittest.TestSuite()
suite.addTest(TestAdd("test_add1"))
suite.addTest(TestAdd("test_add2"))
suite.addTest(TestSub("test_sub1"))
suite.addTest(TestSub("test_sub2")) #运行测试集合
runner = unittest.TextTestRunner()
runner.run(suite)

上例中创建了TestAdd()和TestSub()两个测试类,分别测试计算器中的add()和sub()两个功能。通过TestSuite类的addTest()方法把不同测试类中的测试方法组装到测试套装中。

运行结果:

通过测试结果可以看到,setUp()和tearDown()方法分别作用于每个测试用例的开始于结束。如果每个类中的setUp()和tearDown()所做的事情是一样的,那是不是可以封装一个自己的测试类呢?

from test.test2 import Count
import unittest class MyTest(unittest.TestCase): def setUp(self):
print("test add start") def tearDown(self):
print("test add end") class TestAdd(unittest.TestCase): def test_add1(self):
j = Count(2,3)
print("add1")
self.assertEqual(j.add(),5) def test_add2(self):
j = Count(41,76)
print("add2")
self.assertEqual(j.add(),117) class TestSub(unittest.TestCase): def test_sub1(self):
j = Count(2,3)
print("sub1")
self.assertEqual(j.sub(),-1) def test_sub2(self):
j = Count(71,46)
print("sub2")
self.assertEqual(j.sub(),25) if __name__ == '__main__': #构造测试集
suite = unittest.TestSuite()
suite.addTest(TestAdd("test_add1"))
suite.addTest(TestAdd("test_add2"))
suite.addTest(TestSub("test_sub1"))
suite.addTest(TestSub("test_sub2")) #运行测试集合
runner = unittest.TextTestRunner()
runner.run(suite)

创建MyTest()类的好处显而易见,对于测试类和测试方法来说,应将注意力放在具体的用例的编写上,无须关心setUp()和tearDown()所做的事情、不过前提条件是setUp()和tearDown()所做的事情是每个用例都需要的。

1.5 discover更多测试用例

随着软件功能的不断增加,对应的测试用例也会呈指数级增长。一个实现几十个功能的项目,对应的单元测试用例可能达到上百个。如果把所有的测试用例都写在一个test.py文件中,那么这个文件会越来越臃肿,后期维护起来也比较麻烦。需要将这些用例按照测试的功能进行拆分,分数到不同的测试文件中。

对上例中test.py文件的测试用例进行拆分,拆分后的目录结构如下:

testpro/

  count.py

  testadd.py

  testsub.py

  runtest.py

文件拆分后的实现代码:

testadd.py:

from test1.calculator import Count
import unittest class TestAdd(unittest.TestCase): def setUp(self):
print("test add start") def tearDown(self):
print("test add end") def test_add1(self):
j = Count(2,3)
print("add1")
self.assertEqual(j.add(),5) def test_add2(self):
j = Count(41,76)
print("add2")
self.assertEqual(j.add(),117) if __name__ == '__main__':
unittest.main()

testsub.py:

from test1.calculator import Count
import unittest class TestSub(unittest.TestCase): def setUp(self):
print("test add start") def tearDown(self):
print("test add end") def test_sub1(self):
j = Count(2,3)
print("sub1")
self.assertEqual(j.sub(),-1) def test_sub2(self):
j = Count(71,46)
print("sub2")
self.assertEqual(j.sub(),25) if __name__ == '__main__':
unittest.main()

接着创建用于执行所有用例的runtest.py文件。

runtest.py:

import unittest
#加载测试文件
from test1.testadd import TestAdd
from test1.testsub import TestSub #构造测试集
suite = unittest.TestSuite() suite.addTest(TestAdd("test_add1"))
suite.addTest(TestAdd("test_add2")) suite.addTest(TestSub("test_sub1"))
suite.addTest(TestSub("test_sub2")) if __name__ == '__main__':
#执行测试
runner = unittest.TextTestRunner()
runner.run(suite)

这样的拆分带来了好处,可以根据不同的功能创建不同的测试文件,甚至是不同的测试目录,测试文件中还可以将不同的小功能划分为不同的测试类,在类下编写测试用例,整体结构更加清晰。

这样的设计看上去很完美,但依然没有解决添加用例的问题,当用例达到成百上千条时,在runtest.py文件中通过addTest()添加/删除测试用例就变得非常麻烦,那么有没有方法让unittest单元测试框架自动识别测试用例呢?答案是肯定的,TestLoader类中提供的discover()方法可以解决这个问题。

TestLoader:该类负责根据各种标准加载测试用例,并将它们返回给测试套件。正常情况下,不需要创建这个类的实例。unittest提供了可以共享的defaultTestLoader类,可以使用其子类和方法创建实例,discover()方法就是其中之一。

discover(start_dir,pattern='test*.py',top_level_dir=None):找到指定目录下所有测试模块,并可递归查到子目录下的测试模块,只有匹配到文件名才能被加载。如果启动的不是顶层目录,那么顶层目录必须单独指定。

start_dir:要测试的模块名或测试用例目录

pattern='test*.py':表示用例文件名的匹配原则。此处匹配文件名以“test”开头的“.py”类型的文件,星号“*”表示任意多个字符

top_level_dir=None:测试模块的顶层目录,如果没有顶层目录,默认为None

现在通过discover()方法重新实现runtest.py文件的功能。

runtest.py:

import unittest

#定义测试用例的目录为当前目录
test_dir = './'
discover = unittest.defaultTestLoader.discover(test_dir,pattern='test*.py') if __name__ == '__main__':
#执行测试
runner = unittest.TextTestRunner()
runner.run(discover)

discover()方法会自动根据测试目录(test_dir)匹配查找测试用例文件(test*.py),并将查找到的测试用例组装到测试套件中,因此,可以直接通过run()方法执行discover,大大简化了测试用例的查找与执行。

Selenium(十五):unittest单元测试框架(一) 初识unittest的更多相关文章

  1. Selenium(十六):unittest单元测试框架(二) 初识unittest(续)

    1. 认识unittest(续) 关于unittest单元测试框架,还有一些问题值得进一步探讨.你可能在前一章的学习过程中产生了一些疑问,也许你会在本节中找到答案. 1.1 用例执行的顺序 用例的执行 ...

  2. Selenium+Python ---- 免登录、等待、unittest单元测试框架、PO模型

    1.免登录在进行测试的过程中难免会遇到登录的情况,给测试工作添加了工作量,本文仅提供一些思路供参考解决方式:手动请求中添加cookies.火狐的profile文件记录信息实现.人工介入.万能验证码.去 ...

  3. Python+selenium之简单介绍unittest单元测试框架

    Python+selenium之简单介绍unittest单元测试框架 一.unittest简单介绍 unittest支持测试自动化,共享测试用例中的初始化和关闭退出代码,在unittest中最小单元是 ...

  4. Selenium基于Python web自动化基础二 -- 免登录、等待及unittest单元测试框架

    一.免登录在进行测试的过程中难免会遇到登录的情况,给测试工作添加了工作量,本文仅提供一些思路供参考解决方式:手动请求中添加cookies.火狐的profile文件记录信息实现.人工介入.万能验证码.去 ...

  5. Selenium(十八):unittest单元测试框架(四) HTML测试报告

    1. HTML测试报告 对测试人员来而言,测试的产出很难衡量.换句话说,测试人员的价值比较难以量化和评估,相信这一点对软件测试人员来说深有体会.我们花费了很多时间与精力所做的自动化测试也是如此.所以, ...

  6. 华为五年自动化测试工程详细解说:unittest单元测试框架

    一.单元测试框架说明 ​ 单元测试是指在编程中,针对程序模块的最小单元(类中的方法)进行正确性检验的测试工作.python+selenium自动化测试中通常使用unittest或者pytest作为单元 ...

  7. Python+Selenium框架设计篇之-简单介绍unittest单元测试框架

    前面文章已经简单介绍了一些关于自动化测试框架的介绍,知道了什么是自动化测试框架,主要有哪些特点,基本组成部分等.在继续介绍框架设计之前,我们先来学习一个工具,叫unittest.       unit ...

  8. Python+Selenium ----unittest单元测试框架

    unittest是一个单元测试框架,是Python编程的单元测试框架.有时候,也做叫做“PyUnit”,是Junit的Python语言版本.这里了解下,Junit是Java语言的单元测试框架,Java ...

  9. Selenium(十七):unittest单元测试框架(三) 脚本分析、编写Web用例

    1. 带unittest的脚本分析 也许你现在心里还有疑问,unittest框架与我们前面所编写的Web自动化测试之间有什么必然联系吗?当然有,既然unittest可以组织.运行测试用例,那么为什么不 ...

随机推荐

  1. 【Spring】只想用一篇文章记录@Value的使用,不想再找其它了(附思维导图)

    1 简介 不得不说,Spring为大家提供许多开箱即用的功能,@Value就是一个极其常用的功能,它能将配置信息注入到bean中去.即使是一个简单的功能,Spring也提供了丰富的注入类型和形式.我经 ...

  2. 吃透Python上下文管理器

    什么是上下文管理器? 我们常见的with open操作文件,就是一个上下文管理器.如: with open(file, 'rb') as f: text = f.read() 那上下文管理器具体的定义 ...

  3. (译)An introduction to Kubernetes

    原文:https://www.jeremyjordan.me/kubernetes/(博客园团队推荐的) 这篇博客文章将对Kubernetes进行介绍,以便您了解该工具背后的动机,含义以及使用方式.在 ...

  4. sqlserver数据库批量插入-SqlBulkCopy

    当想在数据库中插入大量数据时,使用insert 不仅效率低,而且会导致一系列的数据库性能问题 当使用insert语句进行插入数据时.我使用了两种方式: 每次插入数据时,都只插入一条数据库,这个会导致每 ...

  5. MongoDB(七):聚合aggregate

    1. 聚合aggregate 聚合主要用于计算数据,类似sql中的sum().avg() 语法: db.集合名称.aggregate([{管道:{表达式}}]) stu准备的数据: db.stu.in ...

  6. java8新特性,你有用起来了吗?(精编)

      2019年9月19日java13已正式发布,感叹java社区强大,经久不衰.由于国内偏保守,新东西总要放一放,让其他人踩踩坑,等稳定了才会去用.并且企业目的还是赚钱,更不会因为一个新特性去重构代码 ...

  7. Java面试题_第二阶段(Servlet、HTTP、Session、JSP、 Ajax、Filter、JDBC、Mysql、Spring)

    1.1. 描述Servlet调用过程? 答案: (1)在浏览器输入地址,浏览器先去查找hosts文件,将主机名翻译为ip地址,如果找不到就再去查询dns服务器将主机名翻译成ip地址. (2)浏览器根据 ...

  8. MySql事务的简单使用

    4个特性 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节.事务在执行过程中发生错误,会被回滚(rollback)到事务开始前的状态 一致性:在事务开始前和事务结束以 ...

  9. 研究STM32F4的IEEE1558 PTP网络时间同步协议实现,软件是RL-TCPnet V7.X的底层

    这个东西发现挺有意思,刚开始研究没整明白怎么用,实测设置一次时间戳就可以使用了,后面在深入研究下 extern ARM_DRIVER_ETH_MAC Driver_ETH_MAC0; ARM_ETH_ ...

  10. Centos7 死活上不了网

    NAT模式,手动修改ifcfg 如下: # vi /etc/sysconfig/network-scripts/ifcfg-ens33 TYPE=EthernetPROXY_METHOD=noneBR ...