• python单元测试


    单元测试

    定义

    单元测试是指一个自动化的测试:

    • 用来验证一小段代码的正确性
    • 可以快速执行
    • 在独立的环境中执行

    断言函数

    assertEqual
    assertNotEqual
    assertTrue
    assertFalse
    assertIs
    assertIsNot
    assertIsNone
    assertIsNotNone
    assertIn
    assertNotIn
    assertIsInstance
    assertNotIsInstance
    assertRaises
    

    示例一:assertEqual

    class Calculator:
        def add(self, *args):
            ret = 0
            for item in args:
                ret += item
            return ret
    
    from unittest import TestCase
    
    from server.app import Calculator
    
    
    class TestCalculator(TestCase):
        def test_add(self):
            calculator = Calculator()
            expect_result = 10
    
            actual_result = calculator.add(1, 2, 3, 4)
    
            self.assertEqual(expect_result, actual_result)
    

    示例二:assertRaises

    class Service:
        def download_img(self, url: str):
            if url:
                return True
            raise ValueError("url error")
    
    from unittest import TestCase
    from server.app import Service
    
    
    class TestService(TestCase):
        def test_download_img_success(self):
            service = Service()
            ret = service.download_img("http://www.baidu.com/1.png")
            self.assertTrue(ret)
    
        def test_download_img_with_exception(self):
            service = Service()
            with self.assertRaises(ValueError):
                service.download_img("")
    

    Test Fixtures

    测试方法执行之前或者之后执行的函数或者方法被称为Test Fixtures

    • module级别的Fixtures:setUpModule,tearDownModule
    • class级别的Fixtures:setUpClass,tearDownClass
    • method级别的Fixtures:setUp,tearDown
    class Service:
        def download_img(self, url: str):
            if url:
                return True
            raise ValueError("url error")
    
    
    from unittest import TestCase
    from server.app import Service
    
    
    def setUpModule():
        print("执行module前...")
    
    
    def tearDownModule():
        print("执行module后...")
    
    
    class TestService(TestCase):
        @classmethod
        def setUpClass(cls):
            print("执行class前...")
    
        @classmethod
        def tearDownClass(cls):
            print("执行class后...")
    
        def setUp(self):
            self.service = Service()
            print("执行任意测试方法前...")
    
        def tearDown(self):
            print("执行任意测试方法后...")
    
        def test_download_img_success(self):
            ret = self.service.download_img("http://www.baidu.com/1.png")
            self.assertTrue(ret)
    
        def test_download_img_with_exception(self):
            with self.assertRaises(ValueError):
                self.service.download_img("")
    
    
    执行module前...
    执行class...
    执行任意测试方法前...
    执行任意测试方法后...
    执行任意测试方法前...
    执行任意测试方法后...
    执行class...
    执行module后...
    

    Mock

    模拟函数,方法,类的行为。

    • Mock:主要模拟指定的方法和属性

    • MagicMock:Mock的子类,同时模拟了很多Magic方法(__len____str__方法等)

    示例一:

    from unittest.mock import Mock
    
    
    def test_hello():
        hello = Mock()
        hello.find_user.return_value = {
            'name': '旺财',
            'age': 1
        }
        print(hello.find_user())
    
    
    if __name__ == '__main__':
        test_hello()
    
    
    {'name': '旺财', 'age': 1}
    

    示例二:

    class Student:
        def __init__(self, id: int, name: str):
            self.id = id
            self.name = name
    
    
    def find_name_by_id(id):
        pass
    
    
    def save_student(student):
        pass
    
    
    def chang_name(id: int, new_name: str):
        student = find_name_by_id(id)
        if student:
            student.name = new_name
            save_student(student)
    
    
    from unittest.mock import Mock
    from unittest import TestCase
    from server.app import chang_name
    from server import app
    
    
    class TestService(TestCase):
        def test_change_name_with_record(self):
            service.find_name_by_id = Mock()
            student = Mock(id=1, name='旧名字')
            service.find_name_by_id.return_value = student
            service.save_student = Mock()
            chang_name(1, '新名字')
            self.assertEqual('新名字', student.name)
            service.find_name_by_id.assert_called()
            service.save_student.assert_called()
    
        def test_change_name_without_record(self):
            service.find_name_by_id = Mock()
            service.find_name_by_id.return_value = None
            service.save_student = Mock()
            chang_name(1, '新名字')
            # 断言没有被调用
            service.save_student.assert_not_called()
    
    

    patch

    path可以临时用Mock对象替换一个目标(函数,方法,类)。本质还是上一节的Mock操作。

    path可以替换的目标:

    • 目标必须是可import的
    • 是在使用的目标的地方替换而不是替换定义

    path的使用方式:

    • 装饰器的方式
    • 上下文管理器的方式
    • 手动方式

    装饰器模拟(首选)

    class Student:
        def __init__(self, id: int, name: str):
            self.id = id
            self.name = name
    
    
    def find_name_by_id(id):
        pass
    
    
    def save_student(student):
        pass
    
    
    def chang_name(id: int, new_name: str):
        student = find_name_by_id(id)
        if student:
            student.name = new_name
            save_student(student)
    
    
    from unittest.mock import Mock, patch
    from unittest import TestCase
    from server.app import chang_name
    
    
    class TestService(TestCase):
    
        @patch("server.server.save_student")
        @patch("server.server.find_name_by_id")
        def test_change_name_decorator(self, find_name_by_id_mock, save_student_mock):
            student = Mock(id=1, name='旧名字')
            find_name_by_id_mock.return_value = student
            chang_name(1, '新名字')
            self.assertEqual('新名字', student.name)
            find_name_by_id_mock.assert_called()
            save_student_mock.assert_called()
    
    

    上下文管理器模拟

    from unittest.mock import Mock, patch
    from unittest import TestCase
    from server.app import chang_name
    
    
    class TestService(TestCase):
    
        def test_change_name_context(self):
            student = Mock(id=1, name='旧名字')
    
            with patch("server.server.find_name_by_id") as find_name_by_id_mock, patch("server.server.save_student"):
                find_name_by_id_mock.return_value = student
                chang_name(1, '新名字')
            self.assertEqual('新名字', student.name)
    
    

    手动模拟

    from unittest.mock import Mock, patch
    from unittest import TestCase
    from server.app import chang_name
    
    
    class TestService(TestCase):
    
        @patch("server.server.find_name_by_id")
        def test_change_name_manual(self, find_name_by_id_mock):
            student = Mock(id=1, name='旧名字')
            find_name_by_id_mock.return_value = student
    
            pather = patch("server.server.save_student")
            pather.start()
            chang_name(1, '新名字')
            pather.start()
            self.assertEqual('新名字', student.name)
    
    

    测试实例

    path里面的模拟对象已经对所有魔术方法都进行了mock,如果不关心返回值可以不用后续return_value了

    import os.path
    from urllib.request import urlopen, Request
    
    
    def download_img(url: str):
        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 successfully, {file_name}"
    
    
    from unittest.mock import patch, MagicMock
    from unittest import TestCase
    from server.app import download_img
    
    
    # https://www.bilibili.com/video/BV1EK411B7LX/?spm_id_from=333.788&vd_source=35b478ef20f153fb3c729ee792cdf651
    class TestService(TestCase):
    
        # urlopen在方法参数中被mock为urlopen_mock
        # urlopen_mock的返回值是一个urlopen_result_mock
        # urlopen_result_mock的__enter__方法返回值是一个web_file_mock
        # web_file_mock的read方法返回值需要定义
        @patch("server.server.urlopen")
        # 因为在service.service文件中引入了,所以可以直接使用service.service引入Request
        @patch("server.server.Request.__new__")
        def test_download_img_with_exception(self, request_init_mock, urlopen_mock):
            # Setup
            url = 'https://www.google.com/a.png'
            urlopen_result_mock = MagicMock()
            web_file_mock = MagicMock()
            urlopen_mock.return_value = urlopen_result_mock
            urlopen_result_mock.__enter__.return_value = web_file_mock
            web_file_mock.read.return_value = None
    
            with self.assertRaises(Exception):
                download_img(url)
    
        @patch("builtins.open")
        @patch("os.path.basename")
        @patch("server.server.urlopen")
        @patch("server.server.Request.__new__")
        def test_download_img_with_success(self, request_init_mock, urlopen_mock, basename_mock, open_mock):
            # Setup
            url = 'https://www.google.com/a.png'
    
            urlopen_result_mock = MagicMock()
            web_file_mock = MagicMock()
            urlopen_mock.return_value = urlopen_result_mock
            urlopen_result_mock.__enter__.return_value = web_file_mock
            web_file_mock.read.return_value = 'not none'
    
            basename_mock.return_value = 'fff'
    
            ret = download_img(url)
            self.assertEqual("Download image successfully, fff", ret)
    
    

    测试覆盖率

    #统计测试覆盖率
    python -m coverage run -m unittest
    
    #查看覆盖率报告
    python -m coverage report
    
    #生成html格式的覆盖率报告
    python -m coverage html
    

    pytest框架

    起步

    pytest是一个基于python语言的第三方测试框架。

    有以下优点:

    • 语法简单
    • 自动检测测试代码
    • 跳过指定测试
    • 开源

    安装使用

    #安装
    pip install pytest
    
    #运行(自动查找test_*.py,*_test.py测试文件。自动查找测试文件中test_开头的函数和Test开头的类中的test_开头的方法)
    pytest
    pytest -v
    
    #测试指定测试类
    pytest test_xxx.py
    

    常用参数

    -v 输出详细的执行信息,比如文件和用例名称
    -s 输出调试信息,比如print的打印信息
    -x 遇到错误用例立即停止
    

    跳过测试

    @pytest.mark.skip
    @pytest.mark.skipif
    
    import sys
    
    from server.app import Student
    import pytest
    
    
    def skip():
        return sys.platform.casefold() == 'win32'.casefold()
    
    
    # @pytest.mark.skip(reason="暂时跳过")
    @pytest.mark.skipif(condition=skip(), reason="window平台跳过")
    class TestStudent:
        def test_student_create(self):
            student = Student(1, 'bob')
            assert student.id == 1
            assert student.name == 'bob'
    
    
    def test_student_create():
        student = Student(2, 'alice')
        assert student.id == 2
        assert student.name == 'alice'
    

    @pytest.fixture

    class Student():
        def __init__(self, id: int, name: str):
            self.id = id
            self.name = name
    
        def valid_name(self):
            if self.name:
                return 3 < len(self.name) < 10
            return False
    
    
    from server.app import Student
    import pytest
    
    
    @pytest.fixture
    def valid_student():
        student = Student(1, 'Kite')
        yield student
    
    
    @pytest.fixture
    def not_valid_student1():
        student = Student(2, 'abcdefjijklmnopq')
        yield student
    
    
    @pytest.fixture
    def not_valid_student2(not_valid_student1):
        # 这里不能对valid_student的name进行赋值修改哟
        student = Student(3, 'Bob')
        student.name = not_valid_student1.name
        yield student
    
    
    def test_student(valid_student, not_valid_student1, not_valid_student2):
        ret = valid_student.valid_name()
        assert ret
        ret = not_valid_student1.valid_name()
        assert not ret
        ret = not_valid_student2.valid_name()
        assert not ret
    
    

    conftest.py

    作用:使得fixture可以被多个文件中的测试用例复用。

    在tests目录下建立conftest.py文件,这里引入其他文件中的fixture,那么其他用例中就可以使用这些fixture,你也可以定义fixture在这个文件中(但是不推荐哈)

    参数化测试

    # 判断是否是奇数
    def is_odd(x: int):
        return x % 2 != 0
    
    
    import pytest
    
    from server.app import is_odd
    
    
    @pytest.mark.parametrize("num,expect_ret", [(1, True), (2, False)])
    def test_is_odd(num, expect_ret):
        actual_ret = is_odd(num)
        assert expect_ret == actual_ret
    
    

    数据库查询的mock

    import pytest
    from unittest.mock import patch, MagicMock
    from server.controller.message_controller import create_user
    
    
    @pytest.fixture
    def mock_session_scope():
        with patch("server.db.session.session_scope") as mock_session_scope:
            mock_session_scope_return_value = MagicMock()
            mock_session_scope.return_value = mock_session_scope_return_value
    
            session_mock = MagicMock()
            mock_session_scope_return_value.__enter__.return_value = session_mock
    
            yield session_mock
    
    
    def test_create_user(mock_session_scope):
        ret = create_user("alice")
        assert 'ok' == ret
    
    
    def test_create_user_exception(mock_session_scope):
        with pytest.raises(ValueError):
            create_user("")
    
    
    

    覆盖率

    pip install pytest
    pip install pytest-cov
    pytest --cov --cov-report=html
    
  • 相关阅读:
    HBase基础【优化、整合Phoenix(Phoenix简介、安装、shell、JDBC操作、二级索引)、整合hive】
    2022-34~35周(8.15-8.28) 项目问题整理
    嵌入式Linux驱动开发(LCD屏幕专题)(一)
    [附源码]Python计算机毕业设计Djangossm新能源电动汽车充电桩服务APP
    使用 Dapr JS SDK 让 Nest.js 集成 Dapr
    git常用操作汇总
    10分钟带你入门chrome(谷歌)浏览器插件开发
    Spring Security 学习笔记
    mac 解决 vscode 权限不足问题,Insufficient permissions
    SetProxy.bat 设置代理
  • 原文地址:https://blog.csdn.net/qq_32182637/article/details/139727730