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
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值