unittest 单元测试框架
https://blog.csdn.net/huilan_same/article/details/52944782?locationNum=9
单元测试框架并非只能用于代码级别的测试,对于单元测试框架来讲,主要完成三件事:
提供用例组织与执行:当你的测试用例只有几条时,可以不必考虑用例的组织,但是,当测试用例达到成百上千条时,大量的测试用例堆砌在一起,就产生了扩展性与维护性等问题,此时需要考虑用例的规范与组织问题了。单元测试框架就是用例解决这个问题的。
提供丰富的比较方法:不论是功能测试,还是单元测试,在用例执行完成之后都需要将实际结果与预期结果进行比较(断言),从而断定用例是否执行通过。单元测试框架一般会提供丰富的断言方法。例如,判断相等/不等,包含/不包含,True/False 的断言方法等。
提供丰富的日志:当测试用例执行失效时能抛出清晰的失败原因,当所有用例执行完成后能提供丰富的执行结果。例如,总执行时间、失败用例数、成功用例数等。
一般的单元测试框架都会提供这些功能,从单元测试框架的这些特征来看,它同样适用于web自动化用例的开发与执行。
一、认识 unittest
什么是单元测试?单元测试负责对最小的软件设计单元(模块)进行验证,它使用软件设计文档中对模块的描述作为指南,对重要的程序分支进行测试以发现模块中的错误。在Python语言下有诸多单元测试框架,如doctest、unittest、pytest、nose等,unittest框架(原名PyUnit框架)为Python语言自带的单元测试框架。
1.1 认识单元测试
可能读者会问不用单元测试框架能写单元测试么?答案是肯定得,单元测试本身就是通过一段代码去验证另一段代码,所以不用单元测试框架也可以写单元测试,下面就通过例子演示不同测试框架的单元测试。
# calculator.py
class Count(object): """docstring for Count""" def __init__(self, a, b): self.a = int(a) self.b = int(b) # 计算加法 def add(self): return self.a + self.b
根据上面所实现的功能,不用测试框架所编写的单元测试如 test.py
from calculator import Count # 测试两个整数相加 class TestCount: def test_add(self): try: j = Count(2,3) add = j.add() assert(add == 5), "Integer adtion result error!" except AssertionError as e: print(e) else: print("Test pass!") # 执行测试类的测试方法 mytest = TestCount() mytest.test_add()
首先,引入 calculator 文件中的 Count 类:然后再 test_add() 方法中调用 Count 类并传入两个参数 2 和 3;最后调用Count 类中的 add() 方法对两个参数做加法运算,并通过 assert() 方法判断 add() 的返回值是否等于5。如果不相等则抛出自定义的 "Integer adtion result error!" 异常信息,如果相等则打印 “Test pass!”。
----------正确时---------- C:\Users\Administrator\AppData\Local\Programs\Python\Python37\python.exe C:/Users/Administrator/Desktop/py/test.py Test pass! Process finished with exit code 0 ----------错误时---------- C:\Users\Administrator\AppData\Local\Programs\Python\Python37\python.exe C:/Users/Administrator/Desktop/py/test.py Integer adtion result error! Process finished with exit code 0
不难发现这种测试方法存在许多问题。首先,测试程序的写法没有一定的规范可以遵循,十个程序员完全可能写出十种不同的测试程序来,不统一的代码维护起来会十分麻烦。其次,需要编写大量的辅助代码才能进行单元测试,在 test.py 中用于测试的代码甚至比被测试的代码还要多,而且这仅仅是一个测试用例,对一个单元模块来说,只编写一条测试用例显然是不够的。
为了让单元测试代码更容易维护和编写,最好的方式是遵循一定的规范来编写测试用例,这也是单元测试框架诞生的初衷。接下来讲如何通过 unittest 单元测试框架编写单元测试用例。
from calculator import Count import unittest class TestCount(unittest.TestCase): """docstring for TestCount""" 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” 命名开头的测试方法,并自动执行它们。
C:\Users\Administrator\AppData\Local\Programs\Python\Python37\python.exe C:/Users/Administrator/Desktop/py/test.py . test start test end ---------------------------------------------------------------------- Ran 1 test in 0.000s OK Process finished with exit code 0
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 calculator import Count import unittest class TestCount(unittest.TestCase): """docstring for TestCount""" def setUp(self): print("test start") def test_add(self): j = Count(2, 3) self.assertEqual(j.add(), 5) def test_add2(self): j = Count(41, 76) self.assertEqual(j.add(), 117) 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() 方法,采用构造测试集的方法来加载与运行测试用例,实现了有选择地执行测试用例。当然,也可以通过注释的方式注释掉第一条用例,但这种做法并不优雅。
首先,调用unittest 框架的 TestSuite() 类来创建测试套件,通过它所提供的 addTest() 方法来添加测试用例 test_add2()。接着调用 unittest 框架的 TextTestRunner() 类,通过它下面的 run() 方法来运行 suiter 所组装的测试用例。
C:\Users\Administrator\AppData\Local\Programs\Python\Python37\python.exe C:/Users/Administrator/Desktop/py/test.py . test start test end ---------------------------------------------------------------------- Ran 1 test in 0.000s OK Process finished with exit code 0
1.3 断言方法
在执行用例的过程中,最终用例是否执行通过,是通过判断测试得到的世纪结果与预期结果是否相等决定的。unittest 框架的 TestCase 类提供下面这些方法用于测试结果的判断。
方法 | 检查 | 版本 |
assertEqual(a,b) | a==b | |
assertNotEqual(a,b) | a!=b | |
assertTrue(x) | bool(x) is True | |
assertFalse(x) | bool(x) is False | |
assertIs(a,b) | a is b | 3.1 |
assertIsNot(a,b) | a is not b | 3.1 |
assertisNone(x) | x is None | 3.1 |
assertisNotNone(x) | x is not None | 3.1 |
assertIn(a,b) | a in b | 3.1 |
assertNotIn(a,b) | a not in b | 3.1 |
assertIsInstance(a,b) | isinstance(a,b) | 3.2 |
assertNotIsInstance(a,b) | not isinstance(a,b) | 3.2 |
assertEqual(first, second, msg=None)
断言第一个参数和第二个参数是否相等,如果不相等则测试失败。msg 为可选参数,用于定义测试失败时打印的信息。
import unittest class Test(unittest.TestCase): """docstring for Test""" def setUp(self): 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): pass if __name__ == '__main__': unittest.main()
在setUp() 方法中要求用户输入一个数,在test_case() 中通过 assertEqual() 比较输入的数是否等于10,如果不相等则输出 msg 中定义的提示信息。执行结果如下:
Enter a number:5 F ====================================================================== FAIL: test_case (__main__.Test) ---------------------------------------------------------------------- Traceback (most recent call last): File "test.72.py", line 11, in test_case self.assertEqual(self.number, 10, msg="Your input is not 10!") AssertionError: 5 != 10 : Your input is not 10! ---------------------------------------------------------------------- Ran 1 test in 0.813s FAILED (failures=1) ***Repl Closed***
从执行结果看到,输入了一个 5,显然与预期的10不相等,msg所定义的提示信息告诉我们 “Your input is not 10!”。
assertNotEqual(first,second,msg=None)
assertNotEqual() 与 assertEqual() 相反,它用于断言第一个参数与第二个参数是否不相等,如果相等则测试失败。
assertTrue(expr,msg=None)
assertFalse(expr,msg=None)
测试表达式是 true ( 或false )。
下面来实现判断一个数是否为质数的功能,所谓的质数(又叫素数)是指只能被 1 和它本身整除的数。
# 用于判断质数(算法问题先不管)
# count.py
def is_prime(n): if n <= 1: return False for i in range(2,n): if n % i == 0: return False return True
# test.py from count import is_prime import unittest class Test(unittest.TestCase): """docstring for Test""" def setUp(self): print("test start") def test_case(self): self.assertTrue(is_prime(7),msg="Is not prime!") def tearDown(self): print("test end") if __name__ == '__main__': unittest.main()
在调用 is_prime() 函数时分别传不同的值来执行测试用例,在上面的例子中传值为7,显然是一个质数,所以通过 assertTrue() 断言得到的结果为True。
assertIn(first, second, msg=None)
assertNotIn(first, second, msg=None)
断言第一个参数是否在第二个参数中,反过来讲,第二个参数是否包含第一个参数。
import unittest class Test(unittest.TestCase): """docstring for Test""" 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 的一个实例。
在 unittest 中还提供了其他检查比较的方法,因为不常用,所以不再一一介绍。读者可参考Python官方文档 unittest 章节进行学习。
1.4 组织单元测试用例
当我们增加被测功能和相应的测试用例之后,再来看看 unittest 单元测试框架是如何扩展和组织新增的测试用例的。
我们同样以测试 1.1 中的 calculator.py 文件为例,为其扩展 sub() 方法,用来计算两个数相减的结果
# calculator.py # 计算机类 class Count(object): """docstring for 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),所以需要针对新功能编写测试用例,扩展后的 test.py 文件如下:
from calculator import Count import unittest class TestAdd(unittest.TestCase): def setUp(self): print("test add start") def test_add(self): j = Count(2, 3) self.assertEqual(j.add(), 5) def test_add2(self): j = Count(41, 76) 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_sub(self): j = Count(2, 3) self.assertEqual(j.sub(), -1) def test_sub2(self): j = Count(71, 46) self.assertEqual(j.sub(), 25) def tearDown(self): print("test sub end") if __name__ == '__main__': # 构造测试集 suite = unittest.TestSuite() suite.addTest(TestAdd("test_add")) suite.addTest(TestAdd("test_add2")) suite.addTest(TestSub("test_sub")) suite.addTest(TestSub("test_sub2")) # 运行测试集合 runner = unittest.TextTestRunner() runner.run(suite) >>> test add start test add end test add start test add end test sub start test sub end test sub start test sub end 1530790508.6260068 .... ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK
通过测试结果可以看到,setUp() 和 tearDown() 方法分别作用于每个测试用例的开始与结束。如果每个类中的setUp() 和 tearDown() 所做的事情是一样的,那就可以封装一个自己的测试类。
from calculator import Count import unittest class MyTest(object): def setUp(self): print("test case start") def tearDown(self): print("test case end") class TestAdd(MyTest): def test_add(self): j = Count(2, 3) self.assertEqual(j.add(), 5) def test_add2(self): j = Count(41, 76) self.assertEqual(j.add(), 117) class TestSub(unittest.TestCase): def test_sub(self): j = Count(2, 3) self.assertEqual(j.sub(), -1) def test_sub2(self): j = Count(71, 46) self.assertEqual(j.sub(), 25) if __name__ == '__main__': unittest.main()
创建 MyTest() 类的好处显而易见,对于测试类和测试方法来说,应将注意力放在具体用例的编写上,无须关心 setUp() 和 tearDown() 所做的事情。不过,前提条件是 setUp() 和 tearDown() 所做的事情是每个用例都需要的。
1.5 discover 更多测试用例
随着软件功能的不断增加,对应的测试用例也会呈指数级增长。一个实现几十个功能的项目,对应的单元测试用例可能达到上百个。如果把所有的测试用例都写在一个 test.py 文件中,那么这个文件会越来越臃肿,后期维护起来也比较麻烦。需要将这些用例按照所测试的功能进行拆分,分散到不同的测试文件中。
对上例中 test.py 文件的测试用例进行拆分,拆分后的目录结构如下:
testpro/ |--------count.py | |--------testadd.py | |--------testsub.py | |--------runtest.py
# testadd.py from calculator import Count import unittest class TestAdd(unittest.TestCase): def setUp(self): print("test add start") def test_add(self): j = Count(2, 3) self.assertEqual(j.add(), 5) def test_add2(self): j = Count(41, 76) self.assertEqual(j.add(), 117) def tearDown(self): print("test add end") if __name__ == '__main__': unittest.main()
# testsub.py from calculator import Count import unittest class TestSub(unittest.TestCase): def setUp(self): print("test sub start") def test_sub(self): j = Count(2, 3) self.assertEqual(j.sub(), -1) def test_sub2(self): j = Count(71, 46) self.assertEqual(j.sub(), 25) def tearDown(self): print("test sub end") if __name__ == '__main__': unittest.main()
接着创建用于执行所有用例的runtest.py文件
# runtest.py import unittest import testadd import testsub # 构造测试类 suite = unittest.TestSuite() suite.addTest(testadd.TestAdd("test_add")) suite.addTest(testadd.TestAdd("test_add2")) suite.addTest(testsub.TestSub("test_sub")) suite.addTest(testsub.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,大大简化了测试用例的查找与执行。
二、关于unittest还需要知道
2.1 用例执行的顺序
用例的执行顺序涉及多个层级:在多个测试目录的情况下,先执行哪个目录?在多个测试文件的情况下,先执行哪个文件?在多个测试类的情况下,先执行哪个测试类?在多个测试方法(用例)的情况下,先执行哪个测试方法?
import unittest class TestBdd(unittest.TestCase): def setUp(self): print("test TestBdd:") def test_ccc(self): print("test_ccc") def test_aaa(self): print("test_aaa") def tearDown(self): pass class TestAdd(unittest.TestCase): def setUp(self): print("test TestAdd:") def test_bbb(self): print("test_bbb") def tearDown(self): pass if __name__ == '__main__': unittest.main() >>> test TestAdd: test_bbb .test TestBdd: test_aaa .test TestBdd: test_ccc . ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK ***Repl Closed***
unittest 框架默认根据 ASCII 码的顺序加载测试用例,数字与字母的顺序为:0-9,A-Z,a-z。所以,TestAdd 类会优先于 TestBdd 类被执行,test_aaa() 方法会优先于 test_ccc() 被执行,因而它并没有按照用例从上到下的顺序执行。
对于测试目录与测试文件来说,unittest框架同样是按照这个规则来加载测试用例的。
那么可不可以让 test_ccc() 先执行?答案是肯定的,只是不能使用默认的 main()方法了,而是需要通过 TestSuite 类的 addTest() 方法按照一定的顺序来加载。
import unittest class TestBdd(unittest.TestCase): def setUp(self): print("test TestBdd:") def test_ccc(self): print("test_ccc") def test_aaa(self): print("test_aaa") def tearDown(self): pass class TestAdd(unittest.TestCase): def setUp(self): print("test TestAdd:") def test_bbb(self): print("test_bbb") def tearDown(self): pass if __name__ == '__main__': # 构造测试集 suite = unittest.TestSuite() suite.addTest(TestBdd("test_ccc")) suite.addTest(TestAdd("test_bbb")) suite.addTest(TestBdd("test_aaa")) # 执行测试 runner = unittest.TextTestRunner() runner.run(suite) >>> test TestBdd: test_ccc .test TestAdd: test_bbb .test TestBdd: test_aaa . ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK ***Repl Closed***
现在的执行顺序就是 addTest() 方法所加载的顺序。discover() 的加载测试用例的规则与main() 方法相同。所以,我们只能通过测试用例的命名来提高被执行的优先级。例如,将希望先被执行的测试用例命名为 “test_a”,将希望最后执行的测试用例命名为 “test_z”。
2.2 执行多级目录的用例
我们要控制 Web 用例的数量,但是当测试用例达到一定量级时,就要考虑划分目录,比如规划如下目录
test_project/test_case/ |----__intit__.py |----test_bbb/ | |----test_ccc/ | | |----test_c.py | |----test_b.py | |----test_ddd/ | |----test_d.py | |----test_a.py
对于上面的目录结构,如果将 discover() 方法中的 start_dir 参数定义为 “./test_case/”目录,那么只能加载 test_a.py 文件中的测试用例。怎样让 unittest 框架查找到 test_case/ 的子目录中的测试文件呢?方法很简单,在每个子目录下放一个 __init__.py 文件。
2.3 跳过测试和预期失败
在运行测试时,有时需要直接跳过某些测试用例,或者当用例符合某个条件时跳过测试,又或者直接将测试用例设置为失败。unittest 提供了实现这些需求的装饰器。
- unittest.skip(reason)
无条件的跳过装饰的测试,说明跳过测试的原因。
- unittest.skipIf(condition,reason)
如果条件为真,跳过装饰的测试。
- unittest.skipUnless(condition,reason)
除非条件为真,不然就跳过装饰的测试。(即,条件为真不跳过,条件为假则跳过)
- unittest.expectedFailure()
测试标记为失败。不管执行结果是否失败,统一标记为失败。
import unittest class MyTest(unittest.TestCase): def setUp(self): pass def tearDown(self): pass @unittest.skip("直接跳过测试") def test_skip(self): print("test aaa") @unittest.skipIf(3 > 2, "当条件为True时跳过测试") def test_skip_if(self): print("test bbb") @unittest.skipUnless(3 > 2, "当条件为True时执行测试") def test_skip_unless(self): print("test ccc") @unittest.expectedFailure def test_expected_failure(self): assertEqual(2, 3) if __name__ == '__main__': unittest.main()
执行结果如下:
xsstest ccc . ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK (skipped=2, expected failures=1) ***Repl Closed***
这些方法同样可以作用于测试类 ,只需将它们定义在测试类上面即可。
import unittest @unittest.skip("直接跳过该测试类") class MyTest(unittest.TestCase): ......
2.4 fixtures
fixtures 的概念前面已经有过简单的介绍,可以形象地把它看做是夹心饼干外层的两片饼干,这两片饼干就是 setUp/tearDown,中间的心就是测试用例。除此之外,unittest 还提供了更大范围的 fixtures,例如对于测试类和模块的 fixtures。
import unittest def setUpModule(): print("test module sart >>>>>>>>") def tearDownModule(): print("test module end >>>>>>>>") class Test(unittest.TestCase): @classmethod def setUpClass(cls): print("test class start =======>") @classmethod def tearDownClass(cls): print("test class end =======>") def setUp(self): print("test case start ------->") def tearDown(self): print("test case end ------->") def test_case(self): print("test casel") def test_case2(self): print("test case2") if __name__ == '__main__': unittest.main()
执行结果如下:
test module sart >>>>>>>> test class start =======> test case start -------> test casel test case end -------> .test case start -------> test case2 test case end -------> .test class end =======> test module end >>>>>>>> ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK ***Repl Closed***
setUpModule/tearDownModule: 在整个模块的开始与结束时被执行
setUpClass/tearDownClass: 在测试类的开始与结束被执行
setUp/tearDown: 在测试用例的开始与结束时被执行
需要注意的是,setUpClass/tearDownClass 的写法稍微有些不同。首先,需要通过 @classmethod 进行装饰,其次方法的参数为 cls。其实,cls与self 并没有什么特别之处,都只表示类方法的第一个参数,只是大家约定俗成,习惯于这样来命名,当然也可以用abc来代替。
三、带 unittest 的脚本分析
从2017.9月开始,火狐在推出了新版本的浏览器55之后,火狐录制回放的小插件Selenium IDE就无法再使用了。(目前能使用,但是是阉割版。且Chrome也有了。功能相同)
下面是火狐官方的回应:
简单翻译一下,就是Selenium IDE由于是基于旧技术实现,在火狐55及之后的新版本上不再支持了,虽然很好用,但是退出历史舞台了。
这样就对有相似功能的新插件提出了要求,这个插件就是Katalon团队的Katalon Recorder,目前功能与Selenium IDE基本一样,而且Katalon团队对这个小插件的功能还在不断的更新完善中。需要使用Selenium IDE的同学,可以下载Katalon Recorder来做为Selenium IDE的替代。还在使用火狐旧版本,与火狐自动升级做斗争的做法显然是不符合技术趋势的。(虽然目前市面上大部分的教学还是以selenium IDE为主,但是都需要控制火狐浏览器和 selenium IDE的版本)
然而,用 Python 的同学比较悲剧的是,目前 Katalon Recorder 仅支持导出 python2 的脚本文件。。。
四、编写 Web 测试用例
前面用了相当大的篇幅详细介绍了 unittest 单元测试框架,其目的是用它来运行 Web 自动化测试脚本。在此之前,需要简单规划一下测试目录:
test_project/ | |----test_case/ | |----test_baidu.py | |----test_youdao.py | |----report/ | |----login.txt | |----runtest.py
创建Web测试用例
# test_baidu.py from selenium import webdriver import unittest import time class MyTest(unittest.TestCase): def setUp(self): self.driver = webdriver.Firefox() self.driver.maximize_window() self.driver.implicitly_wait(10) self.base_url = "http://www.baidu.com" def test_baidu(self): driver = self.driver driver.get(self.base_url + "/") driver.find_element_by_id("kw").clear() driver.find_element_by_id("kw").send_keys("unittest") driver.find_element_by_id("su").click() time.sleep(2) title = driver.title self.assertEqual(title, "unittest_百度搜索") def tearDown(self): self.driver.quit() if __name__ == '__main__': unittest.main()
# test_youdao.py from selenium import webdriver import unittest import time class MyTest(unittest.TestCase): def setUp(self): self.driver = webdriver.Firefox() self.driver.maximize_window() self.driver.implicitly_wait(10) self.base_url = "http://www.youdao.com" def test_youdao(self): driver = self.driver driver.get(self.base_url + "/") driver.find_element_by_id("query").clear() driver.find_element_by_id("query").send_keys("webdriver") driver.find_element_by_id("qb").click() time.sleep(2) title = driver.title self.assertEqual(title, "webdriver_有道搜索") def tearDown(self): self.driver.close() if __name__ == '__main__': unittest.main()
在test_case/ 目录下分别创建百度搜索 test_baidu.py 和有道搜索 test_youdao.py 测试文件,并在测试文件中编写 Web 自动化测试用例。
runtest.py 文件的创建请参考 本文目录1.5 。唯一需要改动的就是指定新的测试目录为 “./test_project/test_case”,之后就可以通过它来执行 test_case 目录下的测试用例了。
4.1 保存测试结果
怎么把测试结果生成一个 log.txt 文件呢?这里需要借助 dos 命令来实现。
首先打开Windows命令提示符,进入到 .../test_project/ 目录下执行命令。
python runtest.py >> report/log.txt 2>&1
最好直接动用 logging 模块,直接完事最好。具体参考:http://www.cnblogs.com/dongye95/p/9060722.html 其中的 logging 模块部分。