Python - 单元测试

1 Unittest

unittest属于python的内置框架,支持多种自动化测试用例的编写,以及支持用例前置条件和后置数据清理功能也可以将多个测试用例放在测试集中生成测试报告。

参考:https://blog.csdn.net/weixin_53041251/article/details/124394128

2 Pytest

pytest也是基于python的一个单元测试框架,是基于unittest的一个扩展框架,比unittest更加简洁,方便,pytest最主要可以支持第三方插件内容,可以更加高效的完成日常工作。而且pytest也支持unittest的代码框架内容。

3 两者区别

断言方面

Unittest 的断言是根据自身携带的断言内容,提供了assertEqual, assertFalse, assertTrue 等。
pytest的断言就是根据Python自带的assert断言内容进行使用的

import pytest

class Test_o1:
	def test_o1(self):
		a = 'gavin'
		b = 'gavin'
		'''判断两个值是否相等'''
		assert a == b
		'''判断a 是否在 b中'''
		assert a in b

用例执行编写规则

unittest:

  • 可以使用自定义测试类内容,但必须继承unittest.TestCase的方法,
  • 测试用例需要以test_开头的方法进行编写,用例的执行顺序是根据ASCII的顺序进行执行的。
  • unittest中提供了TestCase(测试用例),TestSuite(测试套件),TestLoder(加载用例),TextTestRunner(执行用例)等方法让测试用例更加方便编写,
  • 编写测试用例必须导入unittest模块,执行用例必须带有unittest.main()参数。

pytest:

  • 测试文件名必须以test_开头的py文件或者以*_test.py结尾的py文件,
  • 测试类名必须以Test开头,测试用例必须以test_开头。
  • Pytest可以进行执行unittest的用例,pytest的执行顺序可以通过第三方插件进行定制顺序,默认是通过从上往下的顺序进行执行。
  • pytest的执行用例不需要导入模块。执行用例可以通过命令行的形式进行执行。

前后置操作

unittest:

  • 可以通过setup()和tearDown()的方法来控制用例的前后置操作,并且每条用例执行前后都会执行前后置操作。
  • 通过setupclass()和teardownclass()方法来控制一个class下的所有用例都只执行1次前后置操作。
import unittest

class Test(unittest.TestCase):
    def setUp(self):
        print('unittest前置操作,每次执行用例都会进行执行')

    def tearDown(self):
        print('unittest后置操作,每次执行用例都会进行执行')

    @classmethod
    def setUpClass(cls):
        print('所有用例只执行一次前置操作')

    @classmethod
    def tearDownClass(cls):
        print('所有用例只执行一次后置操作')

    def test_01(self):
        print('用例01')

    def test_02(self):
        print('用例02')

    def test_03(self):
        print('用例03')

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

frontend.py::Test::test_01 
所有用例只执行一次前置操作
PASSED                           [ 33%]
unittest前置操作,每次执行用例都会进行执行
用例01
unittest后置操作,每次执行用例都会进行执行

frontend.py::Test::test_02 
PASSED                                        [66%]
unittest前置操作,每次执行用例都会进行执行
用例02
unittest后置操作,每次执行用例都会进行执行

frontend.py::Test::test_03 
PASSED                                        [100%]
unittest前置操作,每次执行用例都会进行执行
用例03
unittest后置操作,每次执行用例都会进行执行
所有用例只执行一次后置操作

pytest:

  • 模块级别的前后置操作(setup_module,teardown_function)表示模块下的测试用例只执行1次前后置操作。
import pytest

def setup_module():
    print('每次用例执行前,只执行一次测试前置操作')

def teardown_module():
    print('每次用例执行后,只执行一次测试后置操作')

def test01():
    print('用例01')

def test02():
    print('用例02')

def test03():
    print('用例03')
    
if __name__ == '__main__':
    pytest.main(['-vs'])

frontendpytest.py::test01 
每次用例执行前,只执行一次测试前置操作
PASSED                                         [ 33%]
用例01

frontendpytest.py::test02 
PASSED                                         [ 66%]
用例02

frontendpytest.py::test03 
PASSED                                         [100%]
用例03
每次用例执行后,只执行一次测试后置操作
import pytest

@pytest.fixture()
def login():
    print('测试用例前置操作')
    yield
    print('测试用例后置操作')

class Test:

    def test01(self, login):
        print('用例01')

    def test02(self, login):
        print('用例02')

    def test03(self):
        print('用例03')

if __name__ == '__main__':
    pytest.main(['-vs'])

通过执行发现,我们在用例03中没有加入fixture,所有他没有执行一些用例的前置和后置操作。

test_fixture.py::Test::test01 
测试用例前置操作
PASSED                                     [ 33%]
用例01
测试用例后置操作

test_fixture.py::Test::test02 
测试用例前置操作
PASSED                                     [ 66%]
用例02
测试用例后置操作

test_fixture.py::Test::test03 
PASSED                                     [100%]
用例03

setUp, setUpclass, setUpmodule 区别

https://blog.csdn.net/qq_42792477/article/details/102833763

setUp: 类里面,每个测试用例都会调用一遍
setUpclass: 类里面,一个测试用例里面只执行一遍
setUpmodule: 类外面, 提前创建好的内容,之后调用

4 实战操作

unittest:

import unittest

import requests

import HtmlTestRunner

# from HTMLTestRunner import HTMLTestRunner
import ddt

# 天气接口的参数化数据
from .HTMLTestRunner import HTMLTestRunner

data = [{"city": '上海', "key": "331eab8f3481f37868378fcdc76cb7cd", 'result': "查询成功!"},

        {"city": "上海", "key": "331eab8f3481f37868378fcdc76cb7c",
         'result': "错误的请求KEY"},

        {"city": "上", "key": "331eab8f3481f37868378fcdc76cb7cd", 'result': "暂不支持该城市"}]


@ddt.ddt
class Test_(unittest.TestCase):

    def tianqi(self, city, key):
        '''天气接口'''

        data = {

            "key": key,

            "city": city

        }

        r = requests.post(url='http://apis.juhe.cn/simpleWeather/query', data=data)

        return r.json()

    def shenfenzheng(self, cardno, key):
        '''身份证查询接口'''

        data = {

            "cardno": cardno,

            "key": key

        }

        r = requests.post('http://apis.juhe.cn/idcard/index', data=data)

        return r.json()

    @ddt.data(*data)
    def test_01(self, data):
        '''参数化的天气接口'''

        x = self.tianqi(city=data['city'], key=data['key'])

        self.assertEqual(x['reason'], data['result'])

    def test_02(self):
        '''正确的身份证号正确的key'''

        cardno = '130428197411155947'  # 身份证信息通过Faker随机创建

        key = "f40a75704fac353952a6534a18f9f437"

        # 请求查询身份证接口

        a = self.shenfenzheng(cardno, key)

        self.assertIn(a['reason'], '成功的返回')

    @unittest.skip('强制跳过,不需要条件')
    def test_03(self):
        '''正确的身份证号错误的key(跳过用例)'''

        cardno = '130428197411155947'

        key = "f40a75704fac353952a6534a18f9f43"

        a = self.shenfenzheng(cardno, key)

        self.assertEqual(a['reason'], '错误的请求KEY')

    @unittest.skipIf(True, '条件成立时候,进行跳过')
    def test_04(self):
        '''错误的身份证号正确的key(跳过用例)'''

        cardno = '42082120031108929'

        key = "f40a75704fac353952a6534a18f9f437"

        a = self.shenfenzheng(cardno, key)

        self.assertEqual(a['reason'], '请输入正确的15或18位身份证')


if __name__ == '__main__':
    unittest.main()
    # report_path = 'report.html'
    #
    # # suite = unittest.TestLoader().discover("./", "test*.py")
    # # print('--------', suite)
    #
    # # 以只写方式打开测试报告文件
    # fp = open(
    #     r"C:\Users\zhihguo\PycharmProjects\Capgemini\Pytest_1\new_tests\test01.html",
    #     "wb")
    # # 实例化 HTMLTestRunner 对象  stream:open 函数打开的文件流; title:[可选参数],为报告标题; description:[可选参数],为报告描述信息;比如操作系统、浏览器等版本;
    # runner = HTMLTestRunner(stream=fp, title="自动化测试报告", description="Chrome 浏览器")
    # # 执行
    # # runner.run(suite)
    # # 关闭
    # # f.close()
    # #
    # testunit = unittest.TestSuite()
    #
    # # 加载用例
    #
    # testunit.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_))
    #
    # # 执行用例
    #
    # runner.run(testunit)
    #
    # # 关闭报告
    #
    # fp.close()

    ''''''
    # import unittest
    # from HTMLTestRunner import HTMLTestRunner
    #
    # # # 生成测试套件
    # # suite = unittest.TestLoader().discover(".\\", "test*.py")
    # # 以只写方式打开测试报告文件
    # f = open(r"test01.html", "wb")
    # # 实例化 HTMLTestRunner 对象  stream:open 函数打开的文件流; title:[可选参数],为报告标题; description:[可选参数],为报告描述信息;比如操作系统、浏览器等版本;
    # runner = HTMLTestRunner(stream=f, title="自动化测试报告", description="Chrome 浏览器")
    # testunit = unittest.TestSuite()
    #
    # # 加载用例
    #
    # testunit.addTests(unittest.TestLoader().loadTestsFromTestCase(Test_))
    # # 执行
    # runner.run(testunit)
    # # 关闭
    # f.close()

pytest:

import requests

import json

import pytest

# 参数不同值

# 天气接口的参数化数据

data = [
    ({"city": '上海', "key": "331eab8f3481f37868378fcdc76cb7cd", 'result': "查询成功!"}),

    ({"city": "上海", "key": "331eab8f3481f37868378fcdc76cb7c", 'result': "错误的请求KEY"}),

    ({"city": "上", "key": "331eab8f3481f37868378fcdc76cb7cd", 'result': "暂不支持该城市"})]


class TestCase:

    def weather(self, city, key):
        url = 'http://apis.juhe.cn/simpleWeather/query'

        # 查询天气接口参数

        data = {

            'city': city,

            'key': key

        }

        r = requests.post(url, data=data)

        return r.json()

    def shenfenzheng(self, cardno, key):
        '''身份证查询接口'''

        data = {

            "cardno": cardno,

            "key": key

        }

        r = requests.post('http://apis.juhe.cn/idcard/index', data=data)

        return r.json()

    @pytest.mark.parametrize('data', data)
    def test_01(self, data):
        # 调用天气预报接口

        r = TestCase().weather(city=data['city'], key=data['key'])

        assert r['reason'] == data['result']

    def test_02(self):
        '''正确的身份证号正确的key'''

        cardno = '130428197411155947'  # 身份证信息通过Faker随机创建

        key = "f40a75704fac353952a6534a18f9f437"

        # 请求查询身份证接口

        a = self.shenfenzheng(cardno, key)

        assert a['reason'] == '成功的返回'

    @pytest.mark.skip('强制跳过,不需要条件')
    def test_03(self):
        '''正确的身份证号错误的key(跳过用例)'''

        cardno = '130428197411155947'

        key = "f40a75704fac353952a6534a18f9f43"

        a = self.shenfenzheng(cardno, key)

        assert a['reason'] == '错误的请求KEY'

    @pytest.mark.skipif(True, reason='条件成立时候,进行跳过')
    def test_04(self):
        '''错误的身份证号正确的key(跳过用例)'''

        cardno = '42082120031108929'

        key = "f40a75704fac353952a6534a18f9f437"

        a = self.shenfenzheng(cardno, key)

        assert a['reason'] in '请输入正确的15或18位身份证'


if __name__ == '__main__':
    pytest.main(['-s'])

用例二:

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    @property
    def balance(self):
        return self.__balance

    @balance.setter
    def balance(self, new_value):
        if new_value < 0:
            raise ValueError('Balance cannot be negative')

        self.__balance = new_value

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError('The deposit amount should be positive')

        self.balance = self.balance + amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError('The withdraw amount should be positive')

        if amount > self.balance:
            raise ValueError('The balance is not enough')

        self.balance = self.balance - amount

import pytest
from python_unittest.myprj.fixtures.bank_account import BankAccount

'''提供给每个测试用例的属性'''
@pytest.fixture()
def bank_account():
    bank_account = BankAccount(50)
    return bank_account


class TestBankAccount:
	
	'''每个test_ 用例都会重新加载一遍setup 里面函数的内容'''
    # def setup(self):
    #     self.bank_account = BankAccount(50)
    #
    # def teardown(self):
    #     pass
	'''运行测试类只会加载一遍 setup_class 里面函数的内容'''
    # def setup_class(self):
    #     self.bank_account = BankAccount(50)
    #
    # def setdown_class(self):
    #     pass

    def test_deposit_succes(self, bank_account):
        bank_account.deposit(20)
        expected_value = 70
        assert expected_value == bank_account.balance

    # def test_withdrow_success(self):
    #     self.bank_account.withdraw(20)
    #     expected_value = 50
    #     assert expected_value == self.bank_account.balance


if __name__ == '__main__':
    pytest.main(['-vs'])

Mock

unittest.mock 是一个用于在Python中进行单元测试的库,
Mock翻译过来就是模拟的意思,顾名思义这个库的主要功能是模拟一些东西。
它的主要功能是使用mock对象替代掉指定的Python对象,以达到模拟对象的行为。

https://www.jianshu.com/p/7c4c9c68373f

'''product_service.py'''

import os
from urllib.request import Request, urlopen
class ProductService:

    def download_img(self, url):
        site_url = Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        with urlopen(site_url) as web_file:
            img_data = web_file.read()

        if not img_data:
            raise Exception(f'Error: cannot load the image from {url}')

        file_name = os.path.basename(url)
        with open(file_name, 'wb') as file:
            file.write(img_data)

        return f'Download image successful, {file_name}'

测试代码

import unittest
from unittest.mock import patch, MagicMock
from python_unittest.myprj.service.product_service import ProductService

class TestProductService(unittest.TestCase):

    def setUp(self) -> None:
        self.product = ProductService()

    def tearDown(self) -> None:
        self.product = None

    @patch('python_unittest.myprj.service.product_service.urlopen')
    @patch('python_unittest.myprj.service.product_service.Request.__new__')
    def test_download_img_with_exception(self, request_mock, urlopen_mock):
        '''
        1 先将Request() 这个类 的实例 变成 mock
        2 urlopen 的返回值提供给 web_file, 上下文管理的返回值是__enter__
        3 web_file.read() 的 返回值 变成mock
        :param request_mock:
        :return:
        '''
        # Setup
        url = 'http://www.baidu.com/a.jpg'
        urlopen_return_mock = MagicMock()
        urlopen_mock.return_value = urlopen_return_mock

        web_readfile = MagicMock()
        urlopen_return_mock.__enter__.return_value = web_readfile
        web_readfile.read.return_value = None

        with self.assertRaises(Exception):
            # Action
            self.product.download_img(url)
            # Assert

    @patch('builtins.open')
    @patch('os.path.basename')
    @patch('python_unittest.myprj.service.product_service.urlopen')
    @patch('python_unittest.myprj.service.product_service.Request.__new__')
    def test_download_img_with_success(self, request_mock, urlopen_mock,
                                       basename_mock, open_mock):
        '''
        1 先将Request() 这个类 的实例 变成 mock
        2 urlopen 的返回值提供给 web_file, 上下文管理的返回值是__enter__
        3 web_file.read() 的 返回值 变成mock
        :param request_mock:
        :return:
        '''
        # Setup
        url = 'http://www.baidu.com/a.jpg'
        urlopen_return_mock = MagicMock()
        urlopen_mock.return_value = urlopen_return_mock

        web_readfile = MagicMock()
        urlopen_return_mock.__enter__.return_value = web_readfile
        web_readfile.read.return_value = 'Not None'
        basename_mock.return_value = 'fff'
        # Action
        result_name = self.product.download_img(url)
        # Assert
        self.assertEqual('Download image successful, fff', result_name)

第二个例子

'''student_service.py'''

from python_unittest.myprj.domain.student import Student

def find_student_name_by_id(student_id):
    print('query db.....', Student('1', 'Tom'))
    return Student('1', 'Tom')

def save_student():
    print('save db......')
    pass

def change_student_name(student_id, student_name):
    student = find_student_name_by_id(student_id)

    if student:
        student.name = student_name
        save_student()

import unittest

from python_unittest.myprj.service import student_service
from unittest.mock import Mock, patch


class TestStudentService(unittest.TestCase):
    def test_change_name_with_record(self):
        # setUp
        # 使用mock 替换 原有的函数的返回值
        student_service.find_student_name_by_id = Mock()
        student = Mock(id=1, name='Tom')
        student_service.find_student_name_by_id.return_value = student

        student_service.save_student = Mock()

        # Action
        student_service.change_student_name(1, 'Jack')

        # Assert
        self.assertEqual('Jack', student.name)
        # 函数被调用
        student_service.save_student.assert_called()

    def test_change_name_without_record(self):
        # setUp
        student_service.find_student_name_by_id = Mock()
        student = None
        student_service.find_student_name_by_id.return_value = student

        student_service.save_student = Mock()

        # Action
        student_service.change_student_name(1, 'Jack')

        # Assert
        # 函数没有被调用
        student_service.save_student.assert_not_called()


class TestStudentServiceWithPatch(unittest.TestCase):
    @patch('python_unittest.myprj.service.student_service.save_student')
    @patch('python_unittest.myprj.service.student_service.find_student_name_by_id')
    def test_change_name_decorator(self, find_student_mock, save_student_mock):
        # Setup
        student = Mock(id=1, name='gavin')
        find_student_mock.return_value = student

        # Action
        student_service.change_student_name(1, 'Tom')
        # Assert
        self.assertEqual('Tom', student.name)

    @patch('python_unittest.myprj.service.student_service.find_student_name_by_id')
    def test_change_name_contextmanager(self, find_student_mock):
        # Setup
        student = Mock(id=1, name='gavin')
        find_student_mock.return_value = student

        # 上下文管理器
        with patch('python_unittest.myprj.service.student_service.save_student'):
            # Action
            student_service.change_student_name(1, 'Tom')
            # Assert
            self.assertEqual('Tom', student.name)

    @patch('python_unittest.myprj.service.student_service.find_student_name_by_id')
    def test_change_name_manual(self, find_student_mock):
        # Setup
        student = Mock(id=1, name='gavin')
        find_student_mock.return_value = student

        patcher = patch('python_unittest.myprj.service.student_service.save_student')
        patcher.start()
        # Action
        student_service.change_student_name(1, 'Tom')
        patcher.stop()
        # Assert
        self.assertEqual('Tom', student.name)

第三个用例

# function.py
def add_and_multiply(x, y):
    addition = x + y
    multiple = multiply(x, y)
    return (addition, multiple)

def multiply(x, y):
    return x * y + 1

print(add_and_multiply(3, 4))

import unittest
from unittest.mock import Mock, patch
from python_unittest.myprj import functions

class TestFunction(unittest.TestCase):

    def test_add_and_mult_success(self):
        # functions.multiply = Mock()
        a, b = 3, 4
        count, multiply = functions.add_and_multiply(a, b)
        self.assertEqual(7, count)
        self.assertEqual(12, multiply)

    '''
    patch()装饰/上下文管理器可以很容易地模拟类或对象在模块测试。
    在测试过程中,您指定的对象将被替换为一个模拟(或其他对象),并在测试结束时还原。
    这里模拟functions.py文件中multiply()函数。
    '''

    @patch("python_unittest.myprj.functions.multiply")
    def test_add_and_mult_without_multipy_success(self, multipy_mock):
        multipy_mock.return_value = 12
        a, b = 3, 4
        count, multiply = functions.add_and_multiply(a, b)
        # 模拟完全被调用了一次,并且该调用使用了指定的参数
        multipy_mock.assert_called_once_with(3, 4)

        self.assertEqual(7, count)
        self.assertEqual(12, multiply)

断言方法

  • assert_not_called:模拟从未被调用过。
  • assert_called:至少调用了一次模拟。
  • assert_called_once:仅调用了一次模拟。
  • assert_called_with:使用指定的参数调用模拟。
  • assert_called_once_with:模拟完全被调用了一次,并且该调用使用了指定的参数。
  • assert_any_call:已使用指定的参数调用了模拟。

https://blog.csdn.net/weixin_53519100/article/details/116807139

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值