自动化测试之七:unittest单元测试框架

前言:

        单元测试是一项对技术要求很高的工作,只有白盒测试人员和软件开发人员才能胜任,但用单元测试框架做单元测试却十分简单,而且单元测试框架不仅可以用来做单元测试,还适用于不同类型的“自动化”测试,其功能主要有:

(1) 提供用例组织和执行

(2) 提供丰富的断言方法

(3)提供丰富的日志

1 认识 unittest

在python中有很多单元测试框架,如:doctest,unittest,pytest,nose等。现如今,unittest已经被作为一个标准模块放入python开发包中。

1.1 认识单元测试

不用单元测试框架能写单元测试吗,当然可以!单元测试的本质上就是通过一端代码验证另外一段代码,所以不用单元测试框架也可以写出单元测试,例:

以下两个文件都放置在unittest_use包下

cal.py

class Cal():
    """
    计算器类
    """
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def add(self):
        """
        加法
        """
        return self.a + self.b

    def sub(self):
        """
        减法
        """
        return self.a - self.b

    def mul(self):
        """
        乘法
        """
        return self.a * self.b

    def div(self):
        """
        除法
        """
        return self.a / self.b

test_cal.py

from unittest_use.cal import Cal


def test_add():
    c = Cal(3, 5)
    result = c.add()
    print("add", result)
    assert result == 8, "加法运算失败"


def test_sub():
    c = Cal(7, 2)
    result = c.sub()
    print("sub", result)
    assert result == 5, "减法运算失败"


def test_mul():
    c = Cal(3, 3)
    result = c.mul()
    print("mul", result)
    assert result == 10, "乘法运算失败"


def test_div():
    c = Cal(6, 2)
    result = c.div()
    print("div", result)
    assert result == 3, "除法运算失败"


if __name__ == '__main__':
    test_add()
    test_sub()
    test_mul()
    test_div()

运行test_cal.py后结果如下:

这样虽然可以实现测试的效果,但是也存在着问题,比如,我们要自己实现断言失败的信息,最后结果无法统计等。当然,我们可以编写更多的代码来解决问题,但这就偏离了我们做单元测试的初衷,我们应该把重点放在测试本身,而不是其他上面,所以引入单元测试框架就能很好的解决问题。如下:

test_cal_1.py


import unittest
from unittest_use.cal import Cal


class Calculator(unittest.TestCase):
    def test_add(self):
        c = Cal(3, 5)
        result = c.add()
        self.assertEqual(result, 8)

    def test_sub(self):
        c = Cal(7, 2)
        result = c.sub()
        self.assertEqual(result, 5)

    def test_mul(self):
        c = Cal(3, 3)
        result = c.mul()
        self.assertEqual(result, 10)

    def test_div(self):
        c = Cal(6, 2)
        result = c.div()
        self.assertEqual(result, 3)


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

输出结果:

上面测试用例,就是用unittest编写的,但是,使用unittest就要遵循其“规则”!!!!!!!!!!!!!!!!:

(1)创建一个类,类名自己起,这是无所谓的,但是必须要继承unittest模块的TestCase类

(2)创建测试方法,该方法必须要以“test”开头

如上面测试用例所示,测试结果不再使用assert断言方法自己定义失败断言了,而是直接调用assertEqual()方法来断言结果与预期是否相同,该方法由TestCase提供,所以直接self.assertEqual()调用即可。最后使用unittest的main()方法来执行测试,它会按照上面说的两条规则查找测试用例

结果分析:

从上面输出结果可以看到丰富了很多,其中,用点(.)表示一条运行通过的测试用例,用 F 表示一条运行失败的用例,用 E 表示一条运行错误的测试用例, 用 s 表示一条运行跳过的测试用例。

本次统计运行了4条测试用例,用时0.003s,失败(failures)了一条测试用例,并且失败的测试用例也有清晰的说明。

2 重要的概念

在unittest文档中,有四个重要的概念:Test Case,Test Suite, Test Runner, Test Fixture,只有理解了这几个概念,才能理解单元测试的基本特征。

2.1 Test Case

Test Case是最小的测试单元,用于检查特定输入集合的特定返回值。unittest提供了TestCase基类,我们创建的测试类需要继承该基类,它可以用来创建新的测试用例。

2.2 Test Suite

测试套件是测试用例、测试套件或者两者的组合,用于组装一组要运行的测试。unittest提供了TestSuite类来创建测试套件。

2.3 Test Runner

Test Runner是一个组件,用于协调测试的执行并向用户提供结果。Test Runner可以使用图形界面,文本界面或返回特殊值来展示执行测试的结果。unittest提供了TextTestRunner类运行测试用例,为了生成Html格式的测试报告,后面还会选择使用HTMLTestRunner运行类。

2.4 Test Fixture

Test Fixture代表执行一个或多个测试所需的环境准备,以及关联的清理动作。例如,创建临时或代理数据库、目录,或者启动服务器京城。unittest中提供了setUp()/tearDown()、setUpClass()/tearDownClass()等方法来完成这些操作。

在理解了上面的几个概念之后,我们使用新的测试用例来进行测试,如下:

import unittest
from unittest_use.cal import Cal


class Calculator(unittest.TestCase):
    # 测试用例前置动作
    def setUp(self):
        print("this is setUp")

    # 测试用例后置动作
    def tearDown(self):
        print("this is tearDown")

    def test_add(self):
        c = Cal(3, 5)
        result = c.add()
        self.assertEqual(result, 8)

    def test_sub(self):
        c = Cal(7, 2)
        result = c.sub()
        self.assertEqual(result, 5)

    def test_mul(self):
        c = Cal(3, 3)
        result = c.mul()
        self.assertEqual(result, 10)

    def test_div(self):
        c = Cal(6, 2)
        result = c.div()
        self.assertEqual(result, 3)


if __name__ == '__main__':
    # 创建测试套件
    suit = unittest.TestSuite()
    suit.addTest(Calculator("test_add"))
    suit.addTest(Calculator("test_sub"))
    suit.addTest(Calculator("test_mul"))
    suit.addTest(Calculator("test_div"))

    # 创建测试运行器
    runner = unittest.TextTestRunner()
    runner.run(suit)

测试结果如下:

改动说明:

①:在类中增加了setUp和tearDown方法,用于定义测试用例的前置动作和后置动作;

②:测试用例执行的修改,就是 if __name__ == '__main__':下面的修改,这里不再使用unittest的main方法,而是调用了TestSuite类下的addTest方法来添加测试用例。因为一次只能添加一个测试用例,所以需要制定测试类及测试方法,然后再调用TextTestRunner类下面的run运行测试套件。

优缺点:

缺点:比直接使用main方法要麻烦很多

优点:①:测试用例的制定顺序可以由测试套件的添加顺序控制,main方法只能按照测试类,方法的名称来执行测试用例;②:当一个测试文件中有很多的测试用例时,但并不是每次都要执行所有的测试用例,尤其是比较耗时的UI自动化测试,通过测试套件和测试运行器可以灵活的控制要执行的测试用例。

结果分析:

setUp/tearDown作用于每一条测试用例的开始位置和结束位置

3 断言方法

在执行测试用例的过程中,最终测试用例执行成功与否,是通过测试得到的实际结果与预期结果进行比较得到的。unittest框架的TestCase类提供的常用的用于测试结果的断言方法如下:

# 断言 first == second
assertEqual(self, first, second, msg=None)

# 断言 first != second
assertNotEqual(self, first, second, msg=None)

# 断言 bool(expr) is True
assertTrue(self, expr, msg=None)

# 断言 bool(expr) is False
assertFalse(self, expr, msg=None)

# 断言 expr1 is expr2
assertIs(self, expr1, expr2, msg=None)

# 断言 expr1 is not expr2
assertIsNot(self, expr1, expr2, msg=None)

# 断言 obj is None
assertIsNone(self, obj, msg=None)

# 断言 obj is not None
assertIsNotNone(self, obj, msg=None)

# 断言 member in container
assertIn(self, member, container, msg=None)

# 断言 member not in container
assertNotIn(self, member, container, msg=None)

# 断言 obj 是 cls对象
assertIsInstance(self, obj, cls, msg=None)

# 断言 obj 不是 cls对象
assertNotIsInstance(self, obj, cls, msg=None)

断言方法的使用,如下:

import unittest


class TestAssert(unittest.TestCase):

    def test_equal(self):
        self.assertEqual(2 + 3, 5)
        self.assertEqual("li", "li")
        self.assertEqual("python", "python")

    def test_in(self):
        self.assertIn("a", ["a", "b"])
        self.assertIn("he", "hello")

    def test_true(self):
        self.assertTrue(True)
        self.assertTrue(False)


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

结果如下:

4 测试用例的组织与discover方法

你是否发现前面的测试用例有什么问题,或者存在什么缺陷?

首先,一个功能对应一条测试用例显然是不够的,要写多少测试用例取决于你对功能需求与测试方法的理解,其次,是测试用例的划分,建议一个测试类对应一个被测试功能

如下:

我们可以在一个测试文件中定义多个测试类,只要它遵循测试用例的“规则”即可,main方法就可以找到并执行它们,但是,我们要测试的类或方法可能有很多

import unittest
from unittest_use.cal import Cal


class TestAdd(unittest.TestCase):
    """add()方法测试"""

    def test_add_integer(self):
        """测试整数相加"""
        c = Cal(3, 5)
        result = c.add()
        self.assertEqual(result, 8)

    def test_add_str(self):
        """测试字符串整数相加"""
        c = Cal("4", "6")
        result = c.add()
        self.assertEqual(result, 10)

    def test_add_decimal(self):
        """测试小数相加"""
        c = Cal(3.5, 5.3)
        result = c.add()
        self.assertEqual(result, 8)

    # ......


class TestSub(unittest.TestCase):
    pass


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

到此为止我们已经写了好几个测试文件,那么你有没有想过这么多的测试文件,我们能不能一次执行执行多个测试文件呢?

当然可以!!!,下面就介绍一下unittes中的TestLoader类提供的discover方法,它可以从多个文件中查找测试用例。

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

discover(self, start_dir, pattern='test*.py', top_level_dir=None)

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

patterns:测试用例文件名的匹配规则

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

下面使用该方法测试:

这里的test_dir使用的时候需要注意,因为我之前的所有测试文件和下面要测试的文件在同一个包内,我想测试的就是该包内所有的测试文件,所以获取的其父目录的路径!!!!!!!!!!

import unittest
import os
test_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)))
print(test_dir)

suits = unittest.defaultTestLoader.discover(test_dir, pattern="test*.py")


if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    runner.run(suits)

说明:discover会自动根据测试用例目录test_dir查找测试用例文件test*.py,并将找到的测试用例添加到测试套件中,因此,可以直接通过run方法执行测试套件suits。这种方式极大的简化了测试用例的查找,我们需要做的就是按照文件的匹配规则创建测试文件即可

5 关于unittest还需要知道的

5.1 测试用例的执行顺序

测试用例的执行顺序设计多个层级,多个测试目录 > 多个测试文件 > 多个测试类 > 多个测试方法(测试用例)。unittest提供的main方法和discover方法是按照什么顺序查找测试用例的呢?

    unittest默认会根据ASCII码的顺醋加载测试用例(数字与字母的顺序为0-9,A-Z, a-z),它并不是按照测试用例的创建顺序从上到下执行的

    discover方法和main方法的执行顺序都一样,对于测试目录和测试文件来说,上面的规律同样适用。所以,如果想让某个测试文件先执行,可以在命名上加以控制

    除命名外,我们可以通过声明测试套件TestSuite类,通过addTest方法按照一定的顺序来加载测试用例

5.2 执行多级目录的测试用例

当测试用例达到一定量时,就要考虑划分目录,根据不同功能或者其他分类划分不同的目录,那么如果将disvocer方法中的start_dir定义为test1的路径,那么只能加载c.py中的测试用例,那么如何让unittest查找test2和test3目录下的测试文件呢,      那就是将这两个目录改为包就可以了!!!!!!!!!!

5.3 跳过测试和预期失败

在运行处测试用例时,有时候需要直接跳过某些测试用例,或者当测试用例符合某个条件时跳过测试,又或者直接将测试用例设置为失败。unittest提供了实现这些需求的装饰器

# 无条件的跳过装饰的测试,需要说明跳过的原因

unittest.skip(reason)  


# 如果条件为真,则跳过装饰的测试

unittest.skipIf(condition, reason)

# 当条件为真时,执行装饰的测试

unittest.skipUunless(condition, reason)

# 不管执行结果是否失败,都将测试标记为失败

unittest.expectedFailure()

如下:

import unittest

class Test(unittest.TestCase):

    @unittest.skip("直接跳过测试")
    def test_skip(self):
        print("test skip")

    @unittest.skipIf(True, "当条件为真时跳过")
    def test_skipif(self):
        print("test skipif")

    @unittest.skipUnless(True, "当条件为真时执行")
    def test_skip_unless(self):
        print("test skip_unless")

    @unittest.expectedFailure
    def test_expected_failure(self):
        print("test expected failure")


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

结果:

5.4 Fixture

我们可以把Fixture看做是夹心饼干外层的两片饼干,这两片饼干就是setUp/tearDown,中间的夹层就是测试用例,除了我们前面提到过的setUp/tearDown之外,unittest还提供了更大范围的Fixture,如测试类和模块的Fixture。

import unittest

def setUpModule():
    print("test module start...........")

def tearDownModule():
    print("test moudule end..............")

class MyTest(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("case 1")

    def test_casel(self):
        print(" case 2")


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

结果如下:

结果分析:

setUpModule/tearDownModule:在整个模块的开始与结束时被执行
setUpClass/tearDownClass:在测试类的开始与结束时被执行
setUp/tearDown: 在测试用例的开始与结束时被执行

需要注意的是:setUpClass/tearDownClass为类方法,需要通过@classmethod进行装饰

6 编写Web自动化测试

import unittest
from time import sleep
from selenium import webdriver

chrome_driver_path = r"C:\Users\Administrator\Envs\selenuimAutoTest\Lib\site-packages\selenium\webdriver\chrome\chromedriver.exe"


class TestBaiDu(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.driver = webdriver.Chrome(executable_path=chrome_driver_path)
        cls.base_url = "http://www.baidu.com"

    def baidu_search(self, search_key):
        self.driver.get(self.base_url)
        self.driver.find_element_by_id("kw").send_keys(search_key)
        self.driver.find_element_by_id("su").click()
        sleep(2)

    def test_search_selenium(self):
        search_key = "selenium"
        self.baidu_search(search_key)
        self.assertEqual(self.driver.title, search_key + "_百度搜索")

    def test_search_unittest(self):
        search_key = "unittest"
        self.baidu_search(search_key)
        self.assertEqual(self.driver.title, search_key + "_百度搜索")

    @classmethod
    def tearDownClass(cls):
        cls.driver.quit()


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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值