单元测试

基础知识

单元测试概念

  • 单元测试是一个自动化的测试
    • 用于验证代码的正确性
    • 在独立环境中快速执行

unittest 模块

  • 测试文件以test_开头
  • 测试文件中的类以Test开头
  • 类中的测试方法以test_开头
  • 测试类需要继承unittest.TestCase

创建目录结构

1
2
3
4
5
6
class Calculator:
def add(self, *args):
result = 0
for arg in args:
result += arg
return result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import unittest
from myProject.basic_knowledge.calculator import Calculator


class TestCalculator(unittest.TestCase):
def test_add(self):
# Setup
cal = Calculator()
excepted_result = 10

# Action
actual_result = cal.add(5, 5)

# Assert
self.assertEqual(excepted_result, actual_result)

批量测试

  • 安装nose模块和coverage模块
  • 运行方法
    • 运行单个测试文件 python3 -m unittest -v myTest.basic_knowledge.test_calculator
    • 运行所有测试文件 nosetests -v myTest/*
    • 统计测试覆盖率 nosetests --with-coverage --cover-erase -v myTest/*

断言 assert

  • assertEqual()
  • assertTrue()
  • assertFalse()
  • assertRaises() 支持上下文管理器
1
2
3
4
5
6
7
8
class Downloads:
def get_downloads(self, url: str):
if url and url == "https://www.baidu.com":
return True
elif url:
return False
else:
raise Exception("url is empty")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import unittest
from myProject.assert_func.downloads import Downloads


class TestDownloads(unittest.TestCase):
def test_get_downloads_true(self):
# Setup
down = Downloads()
url = "https://www.baidu.com"
excepted_result = True

# Action
actual_result = down.get_downloads(url)

# Assert
self.assertEqual(excepted_result, actual_result)

def test_get_downloads_false(self):
# Setup
down = Downloads()
url = "www.google.com"
excepted_result = False

# Action
actual_result = down.get_downloads(url)

# Assert
self.assertEqual(excepted_result, actual_result)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(venv) ➜  pythonUnitTest nosetests --with-coverage --cover-erase -v myTest/*
test_get_downloads_false (myTest.assert_func.test_downloads.TestDownloads) ... ok
test_get_downloads_true (myTest.assert_func.test_downloads.TestDownloads) ... ok
test_add (myTest.basic_knowledge.test_calculator.TestCalculator) ... ok
test_add2 (myTest.basic_knowledge.test_calculator.TestCalculator) ... ok

Name Stmts Miss Cover
-------------------------------------------------------------
myProject/__init__.py 0 0 100%
myProject/assert_func/__init__.py 0 0 100%
myProject/assert_func/downloads.py 7 1 86%
myProject/basic_knowledge/__init__.py 0 0 100%
myProject/basic_knowledge/calculator.py 6 0 100%
myTest/__init__.py 0 0 100%
myTest/assert_func/__init__.py 0 0 100%
myTest/basic_knowledge/__init__.py 0 0 100%
-------------------------------------------------------------
TOTAL 13 1 92%
----------------------------------------------------------------------
Ran 4 tests in 0.002s

OK

修改了 test_downloads.py 后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import unittest
from myProject.assert_func.downloads import Downloads


class TestDownloads(unittest.TestCase):
def test_get_downloads_true(self):
# Setup
down = Downloads()
url = "https://www.baidu.com"
excepted_result = True

# Action
actual_result = down.get_downloads(url)

# Assert
self.assertEqual(excepted_result, actual_result)

def test_get_downloads_false(self):
# Setup
down = Downloads()
url = "www.google.com"

# Action
actual_result = down.get_downloads(url)

# Assert
self.assertFalse(actual_result)

def test_get_downloads_exception(self):
# Setup
down = Downloads()
url = ""

# Action and Assert
with self.assertRaises(Exception):
down.get_downloads(url)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(venv) ➜  pythonUnitTest nosetests --with-coverage --cover-erase -v myTest/*
test_get_downloads_exception (myTest.assert_func.test_downloads.TestDownloads) ... ok
test_get_downloads_false (myTest.assert_func.test_downloads.TestDownloads) ... ok
test_get_downloads_true (myTest.assert_func.test_downloads.TestDownloads) ... ok
test_add (myTest.basic_knowledge.test_calculator.TestCalculator) ... ok
test_add2 (myTest.basic_knowledge.test_calculator.TestCalculator) ... ok

Name Stmts Miss Cover
-------------------------------------------------------------
myProject/__init__.py 0 0 100%
myProject/assert_func/__init__.py 0 0 100%
myProject/assert_func/downloads.py 7 0 100%
myProject/basic_knowledge/__init__.py 0 0 100%
myProject/basic_knowledge/calculator.py 6 0 100%
myTest/__init__.py 0 0 100%
myTest/assert_func/__init__.py 0 0 100%
myTest/basic_knowledge/__init__.py 0 0 100%
-------------------------------------------------------------
TOTAL 13 0 100%
----------------------------------------------------------------------
Ran 5 tests in 0.002s

OK

Test Fixtures

在执行测试方法之前或者之后的内容称为 Test Fixtures

比如说需要给每个测试方法写一个数据库连接,断开 会很麻烦和造成较大开销

模块级别的 Fixtures

  • setUpModule()
  • tearDownModule()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BankAccount:

def __init__(self, balance: float):
self.balance = balance

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

@balance.setter
def balance(self, value: float):
if value < 0:
raise ValueError("Balance cannot be negative")
self.__balance = value

def deposit(self, amount: float):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount

def withdraw(self, amount: float):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import unittest
from myProject.fixtures.bank_account import BankAccount


def setUpModule():
print("calling setup module")


def tearDownModule():
print("calling teardown module")


class TestBankAccount(unittest.TestCase):
def test_deposit(self):
account = BankAccount(100)
account.deposit(50)
self.assertEqual(150, account.balance)

def test_withdraw(self):
account = BankAccount(100)
account.withdraw(50)
self.assertEqual(50, account.balance)
1
2
3
4
5
6
7
8
9
10
(venv) ➜  pythonUnitTest python3 -m unittest -v myTest.fixtures.test_bank_account
calling setup module
test_deposit (myTest.fixtures.test_bank_account.TestBankAccount) ... ok
test_withdraw (myTest.fixtures.test_bank_account.TestBankAccount) ... ok
calling teardown module

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

类级别的 Fixtures

  • setUpClass(cls)
  • tearDownClass(cls)
1
2
3
4
5
6
7
@classmethod
def setUpClass(cls) -> None:
print("calling setup class")

@classmethod
def tearDownClass(cls) -> None:
print("calling teardown class")
1
2
3
4
5
6
7
8
9
10
11
12
(venv) ➜  pythonUnitTest python3 -m unittest -v myTest.fixtures.test_bank_account
calling setup module
calling setup class
test_deposit (myTest.fixtures.test_bank_account.TestBankAccount) ... ok
test_withdraw (myTest.fixtures.test_bank_account.TestBankAccount) ... ok
calling teardown class
calling teardown module

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

方法级别的 Fixtures

  • setUp()
  • tearDown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import unittest
from myProject.fixtures.bank_account import BankAccount


def setUpModule():
print("calling setup module")


def tearDownModule():
print("calling teardown module")


class TestBankAccount(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
print("calling setup class")

@classmethod
def tearDownClass(cls) -> None:
print("calling teardown class")

def setUp(self) -> None:
self.account = BankAccount(100)
print("calling setup")

def tearDown(self) -> None:
self.account = None
print("calling teardown")

def test_deposit(self):
self.account.deposit(50)
self.assertEqual(150, self.account.balance)

def test_withdraw(self):
self.account.withdraw(50)
self.assertEqual(50, self.account.balance)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(venv) ➜  pythonUnitTest python3 -m unittest -v myTest.fixtures.test_bank_account
calling setup module
calling setup class
test_deposit (myTest.fixtures.test_bank_account.TestBankAccount) ... calling setup
calling teardown
ok
test_withdraw (myTest.fixtures.test_bank_account.TestBankAccount) ... calling setup
calling teardown
ok
calling teardown class
calling teardown module

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Mock

mock 即模拟函数、方法、类的功能,在运行测试代码的时候,有哪些不是真的想要调用的代码块或方法,可以使用 mock 进行模拟调用

unittest.mock模块提供了MockMagicMock两个类

  • Mock用于模拟指定的方法属性
  • MagicMock是 Mock 的子类,用于模拟Magic方法
1
2
3
4
class Student:
def __init__(self, id, name):
self.id = id
self.name = name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from myProject.mock_test.Student import Student


def find_by_stu_id(stuid: int) -> Student:
pass


def save_stu(stu: Student):
pass


def s(stid: int, stuname: str):
stu = find_by_stu_id(stid)
if stu is not None:
stu.name = stuname
save_stu(stu)

现在要测试上面alter_stu_name方法中的代码逻辑,但不调用其他方法包括:find_by_stu_id

save_stu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import unittest
from unittest.mock import Mock

from myProject.mock_test import student_service


class TestStudentService(unittest.TestCase):
def test_alter_stu_name_with_student(self):
# Setup
student_service.find_by_stu_id = Mock()
student = Mock(id=1, name="Zachary")
student_service.find_by_stu_id.return_value = student
student_service.save_stu = Mock()

# Action
student_service.alter_stu_name(1, "Zach")

# Assert
self.assertEqual("Zach", student.name)
student_service.save_stu.assert_called()

def test_alter_stu_name_without_student(self):
# Setup
student_service.find_by_stu_id = Mock()
student_service.find_by_stu_id.return_value = None
student_service.save_stu = Mock()

# Action
student_service.alter_stu_name(1, "Zach")

# Assert
student_service.save_stu.assert_not_called()

patch

帮助我们使用 Mock 替换测试代码块中的某些方法、类的调用

  • patch 可以替换的目标
    • 目标必须是可以 import 的
    • 是在调用的地方替换,原先的定义不进行替换

patch 装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import unittest
from unittest.mock import patch, Mock

from myProject.mock_test import student_service


class TestStudentService(unittest.TestCase):

@patch("myProject.mock_test.student_service.save_stu")
@patch("myProject.mock_test.student_service.find_by_stu_id")
def test_alter_stu_name_with_student_decorator(self, mock_find_by_stu_id, mock_save_stu):
# Setup
student = Mock(id=1, name="Zachary")
mock_find_by_stu_id.return_value = student

# Action
student_service.alter_stu_name(1, "Zach")

# Assert
self.assertEqual("Zach", student.name)
mock_save_stu.assert_called()

上下文管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import unittest
from unittest.mock import patch, Mock

from myProject.mock_test import student_service


class TestStudentService(unittest.TestCase):
@patch("myProject.mock_test.student_service.find_by_stu_id")
def test_alter_stu_name_with_student_context_manager(self, mock_find_by_stu_id):
# Setup
student = Mock(id=1, name="Zachary")
mock_find_by_stu_id.return_value = student

with patch("myProject.mock_test.student_service.save_stu") as mock_save_stu:
# Action
student_service.alter_stu_name(1, "Zach")

# Assert
self.assertEqual("Zach", student.name)
mock_save_stu.assert_called()

手动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import unittest
from unittest.mock import patch, Mock

from myProject.mock_test import student_service


class TestStudentService(unittest.TestCase):

@patch("myProject.mock_test.student_service.find_by_stu_id")
def test_alter_stu_name_with_student_manual(self, mock_find_by_stu_id):
# Setup
student = Mock(id=1, name="Zachary")
mock_find_by_stu_id.return_value = student

patcher = patch("myProject.mock_test.student_service.save_stu")
patcher.start()
# Action
student_service.alter_stu_name(1, "Zach")

# Assert
self.assertEqual("Zach", student.name)
patcher.stop()

测试实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os.path
from urllib.request import Request, urlopen


class ProductService:
def download_img(self, url: str):
site_url = Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urlopen(site_url) as response:
image_date = response.read()

if not image_date:
raise Exception("Error: No image data from url: " + url)

filename = os.path.basename(url)
with open(filename, 'wb') as f:
f.write(image_date)

return f"Downloaded {filename}"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import unittest
from unittest.mock import patch, MagicMock

from myProject.test_example.product_service import ProductService


class TestProductService(unittest.TestCase):
def setUp(self) -> None:
self.service = ProductService()

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

@patch('myProject.test_example.product_service.urlopen')
@patch('myProject.test_example.product_service.Request.__new__')
def test_download_img_with_exception(self, mock_request, mock_urlopen):
# Setup
url = "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
return_mock_urlopen = MagicMock()
response_mock = MagicMock()
mock_urlopen.return_value = return_mock_urlopen
return_mock_urlopen.__enter__.return_value = response_mock
response_mock.read.return_value = None

with self.assertRaises(Exception):
self.service.download_img(url)

@patch('builtins.open')
@patch('os.path.basename')
@patch('myProject.test_example.product_service.urlopen')
@patch('myProject.test_example.product_service.Request.__new__')
def test_download_img_with_success(self, mock_request, mock_urlopen, mock_basename, mock_open):
# Setup
url = "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
return_mock_urlopen = MagicMock()
response_mock = MagicMock()
mock_urlopen.return_value = return_mock_urlopen
return_mock_urlopen.__enter__.return_value = response_mock
response_mock.read.return_value = "value"
mock_basename.return_value = "filename"
excepted_result = f"Downloaded {mock_basename.return_value}"

# Action
result = self.service.download_img(url)

# Assert
self.assertEqual(excepted_result, result)

测试覆盖率

统计的是在单元测试中有多少代码行被执行了

覆盖率 = 执行的代码行/总代码行

  • 统计测试覆盖率 python -m coverage run -m unittest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(venv) ➜  pythonUnitTest python -m coverage run -m unittest
.....calling setup module
calling setup class
calling setup
calling teardown
.calling setup
calling teardown
.calling teardown class
calling teardown module
.......
----------------------------------------------------------------------
Ran 14 tests in 0.009s

OK
  • 查看覆盖率报告 python -m coverage report
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(venv) ➜  pythonUnitTest python -m coverage report
Name Stmts Miss Cover
-----------------------------------------------------------------
myProject/__init__.py 0 0 100%
myProject/assert_func/__init__.py 0 0 100%
myProject/assert_func/downloads.py 7 0 100%
myProject/basic_knowledge/__init__.py 0 0 100%
myProject/basic_knowledge/calculator.py 6 0 100%
myProject/fixtures/__init__.py 0 0 100%
myProject/fixtures/bank_account.py 20 4 80%
myProject/mock_test/Student.py 4 2 50%
myProject/mock_test/__init__.py 0 0 100%
myProject/mock_test/student_service.py 10 2 80%
myProject/test_example/__init__.py 0 0 100%
myProject/test_example/product_service.py 13 0 100%
myTest/__init__.py 0 0 100%
myTest/assert_func/__init__.py 0 0 100%
myTest/assert_func/test_downloads.py 19 0 100%
myTest/basic_knowledge/__init__.py 0 0 100%
myTest/basic_knowledge/test_calculator.py 13 0 100%
myTest/fixtures/__init__.py 0 0 100%
myTest/fixtures/test_bank_account.py 25 0 100%
myTest/mock_test/__init__.py 0 0 100%
myTest/mock_test/test_student_service.py 18 0 100%
myTest/patch_test/__init__.py 0 0 100%
myTest/patch_test/test_student_service.py 26 0 100%
myTest/test_example/__init__.py 0 0 100%
myTest/test_example/test_product_service.py 32 0 100%
-----------------------------------------------------------------
TOTAL 193 8 96%
  • 生成 web 格式的报告 python -m coverage html
1
2
(venv) ➜  pythonUnitTest python -m coverage html
Wrote HTML report to htmlcov/index.html

可以点击查看具体哪里没有测试到

PyTest

基于 Python 的第三方测试框架

安装

pip install pytest

运行

  • 在项目路径下使用 pytest命令
    • 自动查找所有test_*.py或者*_test.py的测试文件
    • 自动查找所有test_开头的文件,Test开头的类中test_开头的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(venv) ➜  pythonUnitTest pytest
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-7.4.4, pluggy-1.2.0
rootdir: /Users/zachary/Documents/PythonCode/pythonUnitTest
collected 14 items

myTest/assert_func/test_downloads.py ... [ 21%]
myTest/basic_knowledge/test_calculator.py .. [ 35%]
myTest/fixtures/test_bank_account.py .. [ 50%]
myTest/mock_test/test_student_service.py .. [ 64%]
myTest/patch_test/test_student_service.py ... [ 85%]
myTest/test_example/test_product_service.py .. [100%]

======================================================= 14 passed in 0.14s =======================================================
  • 使用 pytest -v获得更多详细信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(venv) ➜  pythonUnitTest pytest -v
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-7.4.4, pluggy-1.2.0 -- /Users/zachary/Documents/PythonCode/pythonUnitTest/venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/zachary/Documents/PythonCode/pythonUnitTest
collected 14 items

myTest/assert_func/test_downloads.py::TestDownloads::test_get_downloads_exception PASSED [ 7%]
myTest/assert_func/test_downloads.py::TestDownloads::test_get_downloads_false PASSED [ 14%]
myTest/assert_func/test_downloads.py::TestDownloads::test_get_downloads_true PASSED [ 21%]
myTest/basic_knowledge/test_calculator.py::TestCalculator::test_add PASSED [ 28%]
myTest/basic_knowledge/test_calculator.py::TestCalculator::test_add2 PASSED [ 35%]
myTest/fixtures/test_bank_account.py::TestBankAccount::test_deposit PASSED [ 42%]
myTest/fixtures/test_bank_account.py::TestBankAccount::test_withdraw PASSED [ 50%]
myTest/mock_test/test_student_service.py::TestStudentService::test_alter_stu_name_with_student PASSED [ 57%]
myTest/mock_test/test_student_service.py::TestStudentService::test_alter_stu_name_without_student PASSED [ 64%]
myTest/patch_test/test_student_service.py::TestStudentService::test_alter_stu_name_with_student_context_manager PASSED [ 71%]
myTest/patch_test/test_student_service.py::TestStudentService::test_alter_stu_name_with_student_decorator PASSED [ 78%]
myTest/patch_test/test_student_service.py::TestStudentService::test_alter_stu_name_with_student_manual PASSED [ 85%]
myTest/test_example/test_product_service.py::TestProductService::test_download_img_with_exception PASSED [ 92%]
myTest/test_example/test_product_service.py::TestProductService::test_download_img_with_success PASSED [100%]

======================================================= 14 passed in 0.14s =======================================================
  • 使用 pytest 指定文件路径 对单独某个文件进行测试
1
2
3
4
5
6
7
8
9
10
11
(venv) ➜  pythonUnitTest pytest -v myTest/test_example/test_product_service.py
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-7.4.4, pluggy-1.2.0 -- /Users/zachary/Documents/PythonCode/pythonUnitTest/venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/zachary/Documents/PythonCode/pythonUnitTest
collected 2 items

myTest/test_example/test_product_service.py::TestProductService::test_download_img_with_exception PASSED [ 50%]
myTest/test_example/test_product_service.py::TestProductService::test_download_img_with_success PASSED [100%]

======================================================= 2 passed in 0.05s ========================================================
  • 使用 pytest -s输出调试信息,比如 print 等的打印信息
  • 使用 pytest -x在遇到错误测试的时候 会立即停止
  • 跳过指定测试内容
    • @pytest.mark.skip
    • @pytest.mark.skipif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import sys
import unittest

import pytest

from myProject.mock_test.Student import Student


def skip_condition_macos():
return sys.platform.casefold() == 'darwin'.casefold()


def skip_condition_linux():
return sys.platform.casefold() == 'linux'.casefold()


class TestStudent(unittest.TestCase):
def setUp(self) -> None:
self.student = Student(1, "Zach")

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

@pytest.mark.skip(reason="testing case expired")
def test_alter_stu_name(self):
self.student.name = "Zachary"
self.assertEqual("Zachary", self.student.name)

@pytest.mark.skipif(condition=skip_condition_macos(), reason="currently platform not supported")
def test_alter_stu_id(self):
self.student.id = 2
self.assertEqual(2, self.student.id)
1
2
3
4
5
6
7
8
9
10
11
(venv) ➜  pythonUnitTest pytest -v myTest/pytest/test_student.py
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-7.4.4, pluggy-1.2.0 -- /Users/zachary/Documents/PythonCode/pythonUnitTest/venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/zachary/Documents/PythonCode/pythonUnitTest
collected 2 items

myTest/pytest/test_student.py::TestStudent::test_alter_stu_id SKIPPED (currently platform not supported) [ 50%]
myTest/pytest/test_student.py::TestStudent::test_alter_stu_name SKIPPED (testing case expired) [100%]

======================================================= 2 skipped in 0.01s =======================================================

Fixtures

  • 通过@pytest.fixture定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student:
def __init__(self, id, name):
self.id = id
self.name = name

def alter_name(self, name: str) -> bool:
if 3 < len(name) < 8:
self.name = name
return True
return False

def is_valid_name(self) -> bool:
if self.name:
return 3 < len(self.name) < 8
return False
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pytest

from myProject.pytest_fixture.Student import Student


class TestStudent:
@pytest.fixture
def valid_student(self):
student = Student(1, "Zach")
# 使用yield的好处是:yield前后分别可以执行不同内容,在测试结果前后执行
yield student

def test_alter_stu_name_false(self, valid_student):
# Setup
new_name = "ZacharyBlock"
expected_result = False

# Action
actual_result = valid_student.alter_name(new_name)

# Assert
assert actual_result == expected_result
  • 使用 pytest.fixture实现一个fixture引用另一个fixture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pytest

from myProject.pytest_fixture.Student import Student


class TestStudent:
@pytest.fixture
def valid_student(self):
student = Student(1, "Zach")
yield student

@pytest.fixture
def invalid_student(self, valid_student):
valid_student.name = "ZacharyBlock"
yield valid_student

def test_valid_name_false(self, invalid_student):
# Setup
expected_result = False

# Action
actual_result = invalid_student.is_valid_name()

# Assert
assert actual_result == expected_result
  • 使用 pytest.fixture实现引用多个fixture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import pytest

from myProject.pytest_fixture.Student import Student


class TestStudent:
@pytest.fixture
def valid_student(self):
student = Student(1, "Zach")
yield student

@pytest.fixture
def invalid_student(self):
student = Student(1, "ZacharyBlock")
yield student

def test_valid_student(self, valid_student, invalid_student):
# Setup
expected_result_valid = True
expected_result_invalid = False

# Action
actual_result_valid = valid_student.is_valid_name()
actual_result_invalid = invalid_student.is_valid_name()

# Assert
assert expected_result_valid == actual_result_valid \
and expected_result_invalid == actual_result_invalid

conftest.py

conftest.py使得复用pytest.fixture成为可能

首先在 myProj 文件夹下创建一个 pytest_conftest/student.py

1
2
3
4
5
6
7
8
class Student:
def __init__(self, id, name, gender):
self.id = id
self.name = name
self.gender = gender

def vaild_gender(self) -> bool:
return self.gender and self.gender.casefold() in ["male", "female"]

接着在,myTest 下创建一个 pytest_conftest/male_student_fixture.py

1
2
3
4
5
6
7
8
9
import pytest

from myProject.pytest_conftest.student import Student


@pytest.fixture
def male_student_fixture():
student = Student(1, "Zachary", "male")
yield student

为了使得该male_student_fixture可以得到多个模块的复用

需要在myTest文件目录下创建一个conftest.py文件

1
from myTest.pytest_conftest.male_student_fixture import male_student_fixture

在这之后 任何需要使用male_student_fixture的模块,可以直接使用

1
2
3
4
5
6
7
8
9
10
class TestStudentGender:
def test_valid_gender_true(self, male_student_fixture):
# Setup
expected_result = True

# Action
actual_result = male_student_fixture.vaild_gender()

# Assert
assert expected_result == actual_result

需要使用命令行运行 不知道为什么 pycharm 直接运行会有问题

1
2
3
4
5
6
7
8
9
10
(venv) ➜  pythonUnitTest pytest -v myTest/pytest_conftest/test_student_gender.py
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-7.4.4, pluggy-1.2.0 -- /Users/zachary/Documents/PythonCode/pythonUnitTest/venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/zachary/Documents/PythonCode/pythonUnitTest
collected 1 item

myTest/pytest_conftest/test_student_gender.py::TestStudentGender::test_valid_gender_true PASSED [100%]

======================================================= 1 passed in 0.01s ========================================================

测试用例—参数化

  • 使用parameterized实现
    • pip install parameterized
    • 实现一个判断数字是否为奇数的例子
1
2
3
class Judge:
def is_odd(self, num: int) -> bool:
return num % 2 != 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from parameterized import parameterized

from myProject.pytest_parameterized.judge import Judge


class TestJudge:
@parameterized.expand([[1, True], [2, False], [3, True]])
def test_is_odd(self, num, expected_result):
# Setup
judge = Judge()

# Action
actual_result = judge.is_odd(num)

# Assert
assert expected_result == actual_result
  • 使用pytest实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pytest

from myProject.pytest_parameterized.judge import Judge


class TestJudge:
@pytest.mark.parametrize("num,expected_result", [(1, True), (2, False), (3, True)])
def test_is_odd(self, num, expected_result):
# Setup
judge = Judge()

# Action
actual_result = judge.is_odd(num)

# Assert
assert expected_result == actual_result

更新: 2024-01-23 06:38:01
原文: https://www.yuque.com/zacharyblock/cx2om6/dzbxl6wz2ns1pgpi