文章目录
单元测试
定义
单元测试是指一个自动化的测试:
- 用来验证一小段代码的正确性
- 可以快速执行
- 在独立的环境中执行
断言函数
shell
assertEqual
assertNotEqual
assertTrue
assertFalse
assertIs
assertIsNot
assertIsNone
assertIsNotNone
assertIn
assertNotIn
assertIsInstance
assertNotIsInstance
assertRaises
示例一:assertEqual
python
class Calculator:
def add(self, *args):
ret = 0
for item in args:
ret += item
return ret
python
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
python
class Service:
def download_img(self, url: str):
if url:
return True
raise ValueError("url error")
python
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
python
class Service:
def download_img(self, url: str):
if url:
return True
raise ValueError("url error")
python
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("")
python
执行module前...
执行class前...
执行任意测试方法前...
执行任意测试方法后...
执行任意测试方法前...
执行任意测试方法后...
执行class后...
执行module后...
Mock
模拟函数,方法,类的行为。
-
Mock:主要模拟指定的方法和属性
-
MagicMock:Mock的子类,同时模拟了很多Magic方法(
__len__
,__str__
方法等)
示例一:
python
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}
示例二:
python
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)
python
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的使用方式:
- 装饰器的方式
- 上下文管理器的方式
- 手动方式
装饰器模拟(首选)
python
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)
python
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()
上下文管理器模拟
python
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)
手动模拟
python
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了
python
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}"
python
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)
测试覆盖率
shell
#统计测试覆盖率
python -m coverage run -m unittest
#查看覆盖率报告
python -m coverage report
#生成html格式的覆盖率报告
python -m coverage html
pytest框架
起步
pytest是一个基于python语言的第三方测试框架。
有以下优点:
- 语法简单
- 自动检测测试代码
- 跳过指定测试
- 开源
安装使用
shell
#安装
pip install pytest
#运行(自动查找test_*.py,*_test.py测试文件。自动查找测试文件中test_开头的函数和Test开头的类中的test_开头的方法)
pytest
pytest -v
#测试指定测试类
pytest test_xxx.py
常用参数
python
-v 输出详细的执行信息,比如文件和用例名称
-s 输出调试信息,比如print的打印信息
-x 遇到错误用例立即停止
跳过测试
python
@pytest.mark.skip
@pytest.mark.skipif
python
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
python
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
python
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在这个文件中(但是不推荐哈)
参数化测试
python
# 判断是否是奇数
def is_odd(x: int):
return x % 2 != 0
python
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
python
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