UnitTest 测试框架

🍰 一款Python自带的单元测试框架,适用于单元测试,还可用于Web、Appium、接口自动化测试用例的开发与执行等。

1 框架核心

  • 框架核心
    • TestCase:测试用例。
      • 一个测试用例就是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
      • 包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的销毁(tearDown)。
    • TestSuite:测试套件,即多个测试用例集合在一起,TestSuite可以嵌套TestSuite。
    • TextTestRunner:执行测试用例,包括执行TestSuite或TestCase中的方法。
    • TestLoader:批量执行测试用例(可搜索指定文件夹内指定字母开头的模块)。
    • Fixture:固定装置(两个固定函数,一个初始化时使用,一个结束时使用,即测试环境的搭建和销毁)。

1-1 TestCase

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
# 1、导包,test_case_01.py
import unittest


def test_method0():
print("测试方法1-0")


# 2、自定义测试类,继承unittest.TestCase
class TestCase01(unittest.TestCase):
# 3、测试方法以test_开头,test_method1即一个测试用例
def test_method1(self):
print("测试方法1-1")

# 4、测试用例的执行顺序是按方法名的ASCII值进行排序的
def test_method2(self):
print("测试方法1-2")


if __name__ == "__main__":
# 5、执行用例
# ①若光标(鼠标)放在类后面执行,则执行当前类下的所有测试用例
# ②若光标(鼠标)放在方法后执行,则执行的是当前方法的测试用例
# ③命令行模式执行:verbosity参数控制测试执行的详细程度
# 0(安静模式):只显示执行的测试总数和全局结果
# 1(默认模式):显示执行的测试总数,成功点,失败F,出错E,跳过S
# 2(详细模式):显示每个测试用例的帮助字符串和结果
unittest.main(verbosity=1)

1-2 TestSuite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1、导包,test_suite.py,test_case_02.py、test_case_03.py同test_case_01.py
import unittest
from test_case_01 import TestCase01
from test_case_02 import TestCase02
from test_case_03 import TestCase03

# 2、实例化套件对象
suite = unittest.TestSuite()

# 3、使用套件对象添加测试用例的方法:套件对象.addTest(unittest.makeSuite(测试类名))
suite.addTest(unittest.makeSuite(TestCase01))
suite.addTest(unittest.makeSuite(TestCase02))
suite.addTest(unittest.makeSuite(TestCase03))

# 4、实例化运行对象
runner = unittest.TextTestRunner()

# 5、使用运行对象执行套件对象:运行对象.run(套件对象)
runner.run(suite)

1-3 TestLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1、导包,test_loader.py
import unittest

# 2、实例化TestLoader类,并使用discover方法加载测试用例
suite = unittest.TestLoader().discover("./", "test_c*.py")
# 等效写法:使用defaultTestLoader属性加载测试用例
# suite = unittest.defaultTestLoader.discover("./", "test_c*.py")

# 3、实例化运行对象
runner = unittest.TextTestRunner()

# 4、使用运行对象执行套件对象:运行对象.run(套件对象)
runner.run(suite)

1-4 加载与套件

  • 加载与套件
    • TestLoader用于加载测试用例,而TestSuite用于组织和管理测试用例。
    • 加载用例方法
      • discover("file_path", pattern)·······································从指定目录中加载用例
      • loadTestsFromModule(test_module)·····································模块加载单个测试用例
      • loadTestsFromTestCase(TestCaseClass)·································TestCase类加载单个测试用例
      • loadTestsFromName('test_module.TestCaseClass')·······················从完整路径加载单个测试用例
      • loadTestsFromNames(['test_module.TestCaseClass1', 'test_module.TestCaseClass2'])···加载多个用例
    • 添加用例方法:添加单个用例可以使用suite.addTest(),添加多个用例可以使用suite.addTests()
    • discover()会自动扫描指定目录及其子目录中的测试文件,递归地发现并加载测试用例,无需逐个指定。
    • loadTestsFromXXX()需要显式地指定要加载的测试用例或模块,适合于手动指定加载特定的用例或模块。
    • makeSuite()适用于已经定义好的测试类,而loadTestsFromXXX()适用于需要动态加载测试用例的情况。
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
# 1、导包,test_difference.py
import unittest

# 2、创建一个TestSuite实例
suite = unittest.TestSuite()

# 3、创建一个TestLoader实例
loader = unittest.TestLoader()

# from test_case_01 import TestCase01
# from test_case_02 import TestCase02
# from test_case_03 import TestCase03
# suite.addTest(unittest.makeSuite(TestCase01))
# suite.addTest(unittest.makeSuite(TestCase02))
# suite.addTest(unittest.makeSuite(TestCase03))

# 4、将加载的测试用例添加到TestSuite中
suite.addTests(loader.discover("./", pattern="test_c*.py"))

# import test_case_01
# import test_case_02
# import test_case_03
# suite.addTest(loader.loadTestsFromModule(test_case_01))
# suite.addTest(loader.loadTestsFromModule(test_case_02))
# suite.addTest(loader.loadTestsFromModule(test_case_03))

# from test_case_01 import TestCase01
# from test_case_02 import TestCase02
# from test_case_03 import TestCase03
# suite.addTest(loader.loadTestsFromTestCase(TestCase01))
# suite.addTest(loader.loadTestsFromTestCase(TestCase02))
# suite.addTest(loader.loadTestsFromTestCase(TestCase03))

# suite.addTest(loader.loadTestsFromName("test_case_01.TestCase01"))
# suite.addTest(loader.loadTestsFromName("test_case_02.TestCase02"))
# suite.addTest(loader.loadTestsFromName("test_case_03.TestCase03"))

# suite.addTests(loader.loadTestsFromNames([
# "test_case_01.TestCase01",
# "test_case_02.TestCase02",
# "test_case_03.TestCase03"
# ]))

# 5、运行TestSuite中的测试用例
unittest.TextTestRunner().run(suite)

1-5 测试脚手架

  • 测试脚手架
    • 测试脚手架即Fixture,一种代码结构,可以在测试用例执行前后自动调用指定的函数。
    • 方法级别:每个测试方法执行前都会执行setUp(),执行之后都会执行tearDown()
    • 类级别:每个类执行前执行一次setUpClass(),执行后执行一次tearDownClass()
    • 模块级别:模块执行前执行一次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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# test_fixture.py
import unittest
import requests


def setUpModule():
print("测试模块前置执行-初始操作")


def tearDownModule():
print("测试模块后置执行-清理环境")


class TestWeatherAPI(unittest.TestCase):
# 类方法必须使用@classmethod装饰
@classmethod
def setUpClass(cls):
print("类方法的前置执行-初始操作")

@classmethod
def tearDownClass(cls):
print("类方法的后置执行-清理环境")

def setUp(self):
print("每个用例前置执行-初始操作")

def tearDown(self):
print("每个用例后置执行-清理环境")

def test_weather_api(self):
url = "http://t.weather.sojson.com/api/weather/city/101230201"
response = requests.get(url)
self.assertEqual(response.status_code, 200)

def test_history_api(self):
url = "https://api.oick.cn/lishi/api.php"
response = requests.get(url)
self.assertEqual(response.status_code, 200)


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

2 结果断言

  • 结果断言
    • assertIsNone(obj, msg=None)················验证obj是None,不是则fail
    • assertIsNotNone(obj, msg=None)·············验证obj不是None,是则fail
    • assertIn(member, container, msg=None)······验证container是否包含member
    • assertNotIn(member, container, msg=None)···验证container是否不包含member
    • assertTrue(expr, msg=None)·················验证expr是true,如果为false则fail
    • assertFalse(expr, msg=None)················验证expr是false,如果为true则fail
    • assertEqual(expected, actual, msg=None)····验证expected==actual,不等则fail
    • assertNotEqual(first, second, msg=None)····验证first!=second,如果相等则fail
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
# test_assert.py
import unittest
import requests


class TestAPIResponses(unittest.TestCase):
def test_weather_api(self):
url = "http://t.weather.sojson.com/api/weather/city/101230201"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_history_api(self):
url = "https://api.oick.cn/lishi/api.php"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_assertIn(self):
container = [1, 2, 3, 4, 5]
self.assertIn(3, container, "container包含3")

def test_assertNotIn(self):
container = [1, 2, 3, 4, 5]
self.assertNotIn(6, container, "container不包含6")

def test_assertTrue(self):
self.assertTrue(5 > 3, "5大于3")

def test_assertFalse(self):
self.assertFalse(3 > 5, "3不大于5")

def test_assertEqual(self):
self.assertEqual(5, 5, "5等于5")

def test_assertNotEqual(self):
self.assertNotEqual(5, 3, "5不等于3")


if __name__ == "__main__":
# 执行顺序按方法名的ASCII值排序
unittest.main()

3 跳过用例

  • 跳过用例
    • @unittest.skip(reason)····················无条件跳过
    • @unittest.skipIf(condition, reason)·······当condition为True时跳过
    • @unittest.skipUnless(condition, reason)···当condition为False时跳过
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
# test_skip.py
import unittest
import requests


class TestAPIResponses(unittest.TestCase):
def test_weather_api(self):
url = "http://t.weather.sojson.com/api/weather/city/101230201"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_history_api(self):
url = "https://api.oick.cn/lishi/api.php"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

@unittest.skip("直接跳过")
def test_assertIn(self):
container = [1, 2, 3, 4, 5]
self.assertIn(3, container, "container包含3")

def test_assertNotIn(self):
container = [1, 2, 3, 4, 5]
self.assertNotIn(6, container, "container不包含6")

@unittest.skipIf(5 > 3, "跳过用例")
def test_assertTrue(self):
self.assertTrue(5 > 3, "5大于3")

def test_assertFalse(self):
self.assertFalse(3 > 5, "3不大于5")

@unittest.skipUnless(5 != 3, "跳过用例")
def test_assertEqual(self):
self.assertEqual(5, 5, "5等于5")

def test_assertNotEqual(self):
self.assertNotEqual(5, 3, "5不等于3")


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

4 数据驱动

  • 数据驱动
    • 指数据参数化,即ddt(data-driver tests),以数据来驱动整个测试用例的执行。
    • 代码和数据分离,避免冗余,不写重复的代码逻辑,安装:pip install ddt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# test_ddt.py
import unittest
from ddt import ddt, data, unpack


# 使用数据驱动,要在class前加上@ddt修饰器
@ddt
class TestPhone(unittest.TestCase):
# 多参数数据驱动要用列表形式
@data(["admin", "18850000000"])
# @unpack装饰器进行拆包,否则会将数据全部传入一个参数中
# 若存在n个多参数的数据,全部写入@data会很麻烦,于是引出ddt
# 将数据放入一个文本文件(txt、csv、json、yaml、excel)中,从文件中读取
@unpack
def test_phone_identify(self, username, phone):
print("测试:", username, phone)


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

4-1 txt

1
2
3
4
5
6
<!-- ./file/phone.txt -->
admin, 18850000000
tester, 18851000000
devops, 18852000000
leader, 18853000000
pm, 18854000000
  • Txt文件驱动:一行表示一组数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# test_txt.py
import unittest


def read_txt():
phone_list = []
with open("./file/phone.txt", "r", encoding="utf-8") as f:
for line in f.readlines():
phone_list.append(line.strip("\n").split(","))
return phone_list


class TestPhone(unittest.TestCase):
def test_phone_identify(self):
phone = read_txt()
print(phone)


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

4-2 csv

1
2
3
4
5
6
<!-- ./file/phone.csv -->
admin, 18850000000
tester, 18851000000
devops, 18852000000
leader, 18853000000
pm, 18854000000

(1) test_csv1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# test_csv1.py
import csv
import unittest


def read_csv():
phone_list = []
# 使用csv API的reader方法
data = csv.reader(open("./file/phone.csv", "r"))
next(data, None)
for line in data:
phone_list.append(line)
return phone_list


class TestPhone(unittest.TestCase):
def test_phone_identify(self):
phone = read_csv()
print(phone)


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

(2) test_csv2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# test_csv2.py
import csv
import unittest


def read_csv():
phone_list = []
with open("./file/phone.csv", "r", encoding="utf-8") as f:
filename = csv.reader(f)
next(filename, None)
for r in filename:
phone_list.append(r)
return phone_list


class TestPhone(unittest.TestCase):
def test_phone_identify(self):
phone = read_csv()
print(phone)


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

4-3 json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ./file/phone.json
[
{
"username": "admin",
"phone": "18850000000"
},
{
"username": "tester",
"phone": "18851000000"
},
{
"username": "devops",
"phone": "18852000000"
},
{
"username": "leader",
"phone": "18853000000"
},
{
"username": "pm",
"phone": "18854000000"
}
]
  • @data()中的*是传入元组,如果是**,则传入字典。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# test_json.py
import json
import unittest
from ddt import ddt, data, unpack


def read_json():
with open("./file/phone.json", "r", encoding="utf-8") as f:
result = json.load(f)
return result


@ddt
class TestPhone(unittest.TestCase):
# 多参数数据驱动
@data(*read_json())
@unpack
def test_phone_identify(self, username, phone):
print(username, phone)


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

4-4 yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ./file/phone.yaml
-
username: admin
phone: 18850000000
-
username: tester
phone: 18851000000
-
username: devops
phone: 18852000000
-
username: leader
phone: 18853000000
-
username: pm
phone: 18854000000
  • file_data()装饰器:可以直接读取yaml和json文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# test_yaml.py
import unittest
from ddt import ddt, unpack, file_data


@ddt
class TestPhone(unittest.TestCase):
# file_data()装饰器可直接读取yaml和json文件
@file_data("./file/phone.yaml")
@unpack
def test_phone_identify(self, username, phone):
print(username, phone)


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

4-5 excel

1
2
3
4
5
6
<!-- phone.xlsx -->
admin, 18850000000
tester, 18851000000
devops, 18852000000
leader, 18853000000
pm, 18854000000
  • Excel文件驱动:需要使用到openpyxl库,安装库pip install openpyxl
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
# test_excel.py
import unittest
import openpyxl
from ddt import ddt, unpack, data


def read_excel():
xlsx = openpyxl.load_workbook("./file/phone.xlsx")
sheet1 = xlsx["phone"]
print(sheet1.max_row)
print(sheet1.max_column)
print("***********")
phone_list = []
for row in range(2, sheet1.max_row + 1):
row_list = []
for column in range(1, sheet1.max_column + 1):
row_list.append(sheet1.cell(row, column).value)
phone_list.append(row_list)
return phone_list


@ddt
class TestPhone(unittest.TestCase):
@data(*read_excel())
@unpack
def test_phone_identify(self, username, phone):
print(username, phone)


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

5 日志操作

  • 日志操作
    • 记录测试用例的执行情况,排查错误,使用Python自带的logging模块。
    • logging.debug()········调试日志,诊断问题时使用,最详细的日志
    • logging.info()·········普通日志,确定程序是否按照预期的流程执行
    • logging.warning()······警告日志,可能会出现的问题,程序仍可执行
    • logging.error()········错误日志,某些功能软件可能无法正确地执行
    • logging.critical()·····严重错误,表明程序本身可能无法再继续执行

5-1 导出日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1、导入模块,test_logbasic.py
import logging

# 2、设置日志过滤阈值、日志格式
# ①日志是否会被处理,通过阈值进行过滤,默认过滤阈值是warning=30
# ②当小于warning的30时,日志不处理
# ③debug=10、info=20、warning=30、error=40、critical=50
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(message)s [%(name)s] "
"[%(filename)s (%(funcName)s:%(lineno)d)] %(levelname)s",
# 3、导出到日志文件中
filename="./file/test_logbasic.log"
)

logging.debug("调试日志")
logging.info("普通日志")
logging.warning("警告日志")
logging.error("错误日志")
logging.critical("严重错误")

5-2 日志处理

  • 日志处理
    • 应用程序中的不同部分通过获取相同的日志记录器实例来记录日志。
    • 日志记录器(Logger)创建日志消息,并将日志消息传递给已配置的日志处理器(Handler)。
    • 日志处理器将日志输出到指定的终端,同时应用格式化器(Formatter)对日志进行格式化。
    • 格式化后的日志消息以统一格式输出到指定的终端,以便进行跟踪和诊断应用程序的行为。
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
# 1、导入模块,test_logsenior.py
import logging

# 2、创建日志记录器,指定日志记录器的名称,日志记录器可以设置等级,创建日志时即生效
logger = logging.getLogger("Log")
logger.setLevel(logging.DEBUG)

# 3、创建日志处理器,写入文件中,日志处理器也可以设置等级,只输出WARNING以上级别日志
file_handler = logging.FileHandler(filename="./file/test_logsenior.log", encoding="utf-8")
file_handler.setLevel(logging.WARNING)

# 4、创建控制台处理器,打印日志
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)

# 5、创建格式化器
formater = logging.Formatter(
fmt="%(asctime)s - %(message)s [%(name)s] "
"[%(filename)s (%(funcName)s:%(lineno)d)] %(levelname)s"
)

# 6、将格式化器添加到日志处理器上
file_handler.setFormatter(formater)
console_handler.setFormatter(formater)

# 7、将日志处理器添加到日志记录器上
logger.addHandler(file_handler)
logger.addHandler(console_handler)

logger.debug("调试日志")
logger.info("普通日志")
logger.warning("警告日志")
logger.error("错误日志")
logger.critical("严重错误")

6 测试报告

  • 测试报告
    • Text测试报告:使用unittest.TextTestRunner()方法生成Text类型的测试报告。
    • 自带的测试报告:Pycharm执行结果Run中的Export Test Results进行设置生成。
    • XTestRunner测试报告,使用命令安装XTestRunner第三方库:pip install XTestRunner
    • BeautifulReport测试报告,需命令安装BeautifulReport库:pip install BeautifulReport
    • HTMLTestRunner测试报告,需要手动下载并修改HTMLTestRunner.py文件内容,再放入Lib中。

6-1 Text测试报告

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
# test_textreport.py
import unittest
import requests


class TestAPIResponses(unittest.TestCase):
def test_weather_api(self):
url = "http://t.weather.sojson.com/api/weather/city/101230201"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_history_api(self):
url = "https://api.oick.cn/lishi/api.php"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_assertIn(self):
container = [1, 2, 3, 4, 5]
self.assertIn(3, container, "container包含3")

def test_assertNotIn(self):
container = [1, 2, 3, 4, 5]
self.assertNotIn(6, container, "container不包含6")

def test_assertTrue(self):
self.assertTrue(5 > 3, "5大于3")

def test_assertFalse(self):
self.assertFalse(3 > 5, "3不大于5")

def test_assertEqual(self):
self.assertEqual(5, 5, "5等于5")

def test_assertNotEqual(self):
self.assertNotEqual(5, 3, "5不等于3")


if __name__ == "__main__":
# 创建测试套件
suite = unittest.TestLoader().loadTestsFromTestCase(TestAPIResponses)
# 需要使用命令“python test_textreport.py”执行测试,才能生成测试报告
with open("./reports/test_textreport.txt", "w") as f:
runner = unittest.TextTestRunner(f)
runner.run(suite)

6-2 XTestRunner

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
48
49
50
51
52
53
54
55
56
# test_xtesttunner.py
import unittest
import requests
from XTestRunner import HTMLTestRunner


class TestAPIResponses(unittest.TestCase):
def test_weather_api(self):
url = "http://t.weather.sojson.com/api/weather/city/101230201"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_history_api(self):
url = "https://api.oick.cn/lishi/api.php"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_assertIn(self):
container = [1, 2, 3, 4, 5]
self.assertIn(3, container, "container包含3")

def test_assertNotIn(self):
container = [1, 2, 3, 4, 5]
self.assertNotIn(6, container, "container不包含6")

def test_assertTrue(self):
self.assertTrue(5 > 3, "5大于3")

def test_assertFalse(self):
self.assertFalse(3 > 5, "3不大于5")

def test_assertEqual(self):
self.assertEqual(5, 5, "5等于5")

def test_assertNotEqual(self):
self.assertNotEqual(5, 3, "5不等于3")


if __name__ == "__main__":
# 创建测试套件
suite = unittest.TestLoader().loadTestsFromTestCase(TestAPIResponses)
# 需要使用命令“python test_xtesttunner.py”执行测试,才能生成测试报告
with open("./reports/test_xtesttunner.html", "wb") as f:
runner = HTMLTestRunner(
# verbosity参数:0是简单报告,1是一般报告,2是详细报告
stream=f, verbosity=2, title="UnitTest测试报告",
description="UnitTest测试报告:内容描述..."
)
result = runner.run(suite)

# with open("./reports/test_xtesttunner.html", "wb") as f:
# unittest.main(testRunner=HTMLTestRunner(
# stream=f,
# title="UnitTest测试报告",
# description="UnitTest测试报告:内容描述..."
# ))

6-3 BeautifulReport

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
# test_beautifulreport.py
import unittest
import requests
from BeautifulReport import BeautifulReport


class TestAPIResponses(unittest.TestCase):
def test_weather_api(self):
url = "http://t.weather.sojson.com/api/weather/city/101230201"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_history_api(self):
url = "https://api.oick.cn/lishi/api.php"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_assertIn(self):
container = [1, 2, 3, 4, 5]
self.assertIn(3, container, "container包含3")

def test_assertNotIn(self):
container = [1, 2, 3, 4, 5]
self.assertNotIn(6, container, "container不包含6")

def test_assertTrue(self):
self.assertTrue(5 > 3, "5大于3")

def test_assertFalse(self):
self.assertFalse(3 > 5, "3不大于5")

def test_assertEqual(self):
self.assertEqual(5, 5, "5等于5")

def test_assertNotEqual(self):
self.assertNotEqual(5, 3, "5不等于3")


if __name__ == "__main__":
# 创建测试套件
suite = unittest.TestLoader().loadTestsFromTestCase(TestAPIResponses)
# 需要使用命令“python test_beautifulreport.py”执行测试,才能生成测试报告
BeautifulReport(suite).report(
# description在BeautifulReport报告中是用例名称
description="UnitTest接口测试",
filename="./reports/test_beautifulreport.html"
)

6-4 HTMLTestRunner

  • 命令安装HTMLTestRunner第三方库会报错,该库由Python2编写,没有版本更新。
1
2
3
4
pip install HTMLTestRunner
Defaulting to user installation because normal site-packages is not writeable
ERROR: Could not find a version that satisfies the requirement HTMLTestRunner (from versions: none)
ERROR: No matching distribution found for HTMLTestRunner
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
48
49
50
51
52
53
54
55
# test_htmltestrunner.py
import unittest
import requests
from HTMLTestRunner import HTMLTestRunner


class TestAPIResponses(unittest.TestCase):
def test_weather_api(self):
url = "http://t.weather.sojson.com/api/weather/city/101230201"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_history_api(self):
url = "https://api.oick.cn/lishi/api.php"
response = requests.get(url)
self.assertIsNotNone(response.json(), "响应非空")

def test_assertIn(self):
container = [1, 2, 3, 4, 5]
self.assertIn(3, container, "container包含3")

def test_assertNotIn(self):
container = [1, 2, 3, 4, 5]
self.assertNotIn(6, container, "container不包含6")

def test_assertTrue(self):
self.assertTrue(5 > 3, "5大于3")

def test_assertFalse(self):
self.assertFalse(3 > 5, "3不大于5")

def test_assertEqual(self):
self.assertEqual(5, 5, "5等于5")

def test_assertNotEqual(self):
self.assertNotEqual(5, 3, "5不等于3")


if __name__ == "__main__":
# 创建测试套件
suite = unittest.TestLoader().loadTestsFromTestCase(TestAPIResponses)
# 需要使用命令“python test_htmltestrunner.py”执行测试,才能生成测试报告
with open("./reports/test_htmltestrunner.html", "wb") as f:
runner = HTMLTestRunner(
stream=f, verbosity=2, title=u"UnitTest测试报告",
description=u"UnitTest测试报告:内容描述..."
)
result = runner.run(suite)

# with open("./reports/test_htmltestrunner.html", "wb") as f:
# unittest.main(testRunner=HTMLTestRunner(
# stream=f,
# title="UnitTest测试报告",
# description="UnitTest测试报告:内容描述..."
# ))

7 配置解析

  • 配置解析
    • ini:Initialization,是Windows系统常见的配置文件格式,使用简单的键值对结构来存储配置信息。
    • conf:Configuration,通常用于Unix或Linux系统中,也是文本文件,用于存储应用程序的配置信息。
    • cnf:Configuration,通常用于MySQL数据库中,也是文本文件,用于存储数据库服务器的配置信息。
    • cfg:Configuration,通用的配置文件格式,采用了不同的结构和语法,存储各种应用程序的配置信息。
    • yaml:与yml同种格式,扩展名不同,是一种可读的数据序列化标准,使用缩进和换行来表示数据结构。

7-1 ini格式

1
2
3
4
5
6
7
8
; ./file/conf.ini
[project]
base_url = http://127.0.0.1:5000/

[log]
name = pylog
filename = logs/test_log.log
debug = false

7-2 ini解析

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
# test_ini.py
# conf、cnf、cfg格式的配置文件,也可以使用configparser库进行解析
# 文件解析之前,需要将对应文件的扩展名改为.ini,然后按以下操作进行
import configparser

# 创建一个ConfigParser对象
config = configparser.ConfigParser()
# 读取配置文件
config.read("./file/conf.ini", encoding="utf-8")

# 获取所有的段名,并打印输出
section = config.sections()
print(section)

# 获取指定段名下的name配置,并打印输出
filename = config.get("log", "filename")
print(filename)

# 字典模式取值
print(config["project"]["base_url"])

# 获取指定段名下的配置名,并打印输出
options = config.options("log")
print(options)

# 获取指定段名下的配置二元元组,并打印输出
options_tuple = config.items("log")
print(options_tuple)

# 转换值的类型,默认情况下,配置的值全部会被转换成字符串
print(config.getboolean("log", "debug"))

# 修改配置项的值
config.set("log", "name", "test_log")

# 写入配置文件
with open("./file/conf.ini", "w") as configfile:
config.write(configfile)

7-3 yml解析

1
2
3
4
5
6
7
# test_yml.py
import yaml

with open("./file/conf.yml", "r", encoding="utf-8") as f:
config = yaml.load(f, Loader=yaml.FullLoader)

print(config)

(1) 数组格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# conf.yml
logs:
- name: logs_array
- filename: logs/test_log.log
- debug: True

# 等价于
# {
# "logs": [{
# "name": "logs_array",
# "filename": "logs/test_log.log",
# "debug": True
# }]
# }

(2) Map对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# conf.yaml
logs:
name: logs_map
filename: logs/test_log.log
debug: True

# 等价于
# {
# "logs":
# {
# "name": "logs_map",
# "filename": "logs/test_log.log",
# "debug": True
# }
# }

8 项目框架

  • 项目框架
    • 单例模式:导入模块时会执行模块所在的__init__.py文件,将公用模块放入文件代码中,以实现单例模式。
    • 配置文件封装:能处理多种类型的配置文件,返回值数据结构一致,简单功能封装成函数,复杂功能封装成类。
    • 变量配置:项目根目录下单独使用一个Python文件,在其中定义配置信息,其他模块直接引用效率更高更直观。

8-1 基本框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
logs:                               # 存储项目日志
- test_log.log # 测试输出日志
- test_api.log
common: # 存储公用方法
- log_output.py # 日志功能封装
- http_request.py # 请求功能封装
reports: # 存储项目报告
- test_xtesttunner.html
- test_htmltestrunner.html
- test_beautifulreport.html
test_data: # 存储用例数据
- test_api.csv
test_cases: # 存储测试用例
- test_api.py
- main.py # 作为入口函数,方便项目调试

(1) main.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
# main.py,项目入口
import unittest
from XTestRunner import HTMLTestRunner
# from HTMLTestRunner import HTMLTestRunner
# from BeautifulReport import BeautifulReport

if __name__ == "__main__":
# 收集所有的用例,并返回测试套件
suite = unittest.TestLoader().discover("test_cases")

# 执行所有的用例,并生成报告
with open("./reports/test_xtesttunner.html", "wb") as f:
runner = HTMLTestRunner(
# verbosity参数:0是简单报告,1是一般报告,2是详细报告
stream=f, verbosity=2, title="UnitTest测试报告",
description="UnitTest测试报告:内容描述..."
)
result = runner.run(suite)

# with open("./reports/test_htmltestrunner.html", "wb") as f:
# runner = HTMLTestRunner(
# stream=f, verbosity=2, title=u"UnitTest测试报告",
# description=u"UnitTest测试报告:内容描述..."
# )
# result = runner.run(suite)

# BeautifulReport(suite).report(
# # description在BeautifulReport报告中是用例名称
# description="UnitTest接口测试",
# filename="./reports/test_beautifulreport.html"
# )

(2) test_api.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
37
38
39
40
41
# test_api.py
import csv
import unittest
from common.log_output import get_logger
from common.http_request import send_http_request


def read_csv():
phone_list = []
with open("./test_data/test_api.csv", "r", encoding="utf-8") as f:
filename = csv.reader(f)
next(filename, None)
for r in filename:
# 使用strip()方法去除额外的空格
cleaned_data = [item.strip() for item in r]
phone_list.append(cleaned_data)
return phone_list


class TestAPI(unittest.TestCase):
# 将日志相关内容定义为类属性
logger = get_logger("test_api", "logs/test_api.log", debug=True)

@classmethod
def setUpClass(cls) -> None:
cls.logger.info("接口测试开始")

@classmethod
def tearDownClass(cls) -> None:
cls.logger.info("接口测试结束")

def test_api_response(self):
self.logger.info("用例开始测试")
# 1、测试数据
for url, method in read_csv():
# 2、测试步骤
result = send_http_request(url=url, method=method)
self.logger.debug("method: {}, url: {}".format(method, url))
# 3、状态码断言
self.assertEqual(result.status_code, 200)
self.logger.info("用例结束测试")

(3) test_api.csv

1
2
3
4
5
6
7
8
<!-- test_api.csv,需空一行,否则无法获取到第一行数据 -->
<!-- 执行项目时,必须把注释说明删除,否则用例将执行失败 -->

https://api.oick.cn/yulu/api.php, GET
https://api.oick.cn/lishi/api.php, GET
https://api.oick.cn/dutang/api.php, GET
https://v.api.aa1.cn/api/yiyan/index.php, GET
https://api.thecatapi.com/v1/images/search, GET

(4) log_output.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# log_output.py
import logging


def get_logger(name, filename, mode="a", encoding="utf-8", fmt=None, debug=False):
"""
:param name: 日志记录器的名称
:param filename: 日志文件名
:param mode: 文件模式
:param encoding: 字符编码
:param fmt: 日志格式
:param debug: 调试模式
:return:
"""
# 创建一个日志记录器并设置日志等级
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)

# 确定日志文件和控制台输出的日志级别,文件处理器的等级一般情况下比控制台要高
if debug:
file_level = logging.DEBUG
console_level = logging.DEBUG
else:
file_level = logging.WARNING
console_level = logging.INFO

# 定义日志的输出格式
if fmt is None:
fmt = "%(asctime)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] " \
"%(levelname)s - %(message)s"

# 创建日志处理器,写入文件中并设置日志等级
file_handler = logging.FileHandler(filename=filename, mode=mode, encoding=encoding)
file_handler.setLevel(file_level)

# 写入控制台的日志处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(console_level)

# 创建格式化器并添加到日志处理器
formatter = logging.Formatter(fmt=fmt)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

# 将日志处理器添加到日志器上
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# 返回日志
return logger


if __name__ == "__main__":
logger = get_logger(name="test", filename="../logs/test_log.log", debug=True)
logger.debug("调试日志")
logger.info("普通日志")
logger.warning("警告日志")
logger.error("错误日志")
logger.critical("严重错误")

(5) http_request.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
# http_request.py
import requests


# ->requests.Response表示返回的类型,其他模块调用时,Pycharm会提示
def send_http_request(url, method) -> requests.Response:
"""
发送http请求
:param url: 请求路径
:param method: 请求方式
:return: response
"""
try:
if method.upper() == "GET":
response = requests.get(url)
return response
elif method.upper() == "POST":
response = requests.post(url)
return response
else:
raise ValueError("不支持的HTTP方法!")
except requests.RequestException as e:
print(f"错误发生: {e}")


if __name__ == "__main__":
# 方法的调用
result = send_http_request(url="https://www.baidu.com", method="GET")
print(result.status_code)

8-2 单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
logs:                               # 存储项目日志
- test_log.log # 测试输出日志
- test_api.log
common: # 存储公用方法
- __init__.py # 【✔】实现单例模式,将日志输出功能放入其中
- log_output.py # 日志功能封装
- http_request.py # 请求功能封装
reports: # 存储项目报告
- test_xtesttunner.html
- test_htmltestrunner.html
- test_beautifulreport.html
test_data: # 存储用例数据
- test_api.csv
test_cases: # 存储测试用例
- test_api.py #【✔】修改导入模块、类属性定义方法
- main.py # 作为入口函数,方便项目调试

(1) __init__.py

1
2
3
4
5
# __init__.py
from .log_output import get_logger

# 单例模式,避免用例中调用的日志方法或文件,同名时导致重复添加多个日志处理器
logger = get_logger("test_api", "logs/test_api.log", debug=True)

(2) test_api.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
37
38
39
40
41
42
43
# test_api.py
import csv
import unittest
from common import logger
# from common.log_output import get_logger
from common.http_request import send_http_request


def read_csv():
phone_list = []
with open("./test_data/test_api.csv", "r", encoding="utf-8") as f:
filename = csv.reader(f)
next(filename, None)
for r in filename:
# 使用strip()方法去除额外的空格
cleaned_data = [item.strip() for item in r]
phone_list.append(cleaned_data)
return phone_list


class TestAPI(unittest.TestCase):
# 将日志相关内容定义为类属性
# logger = get_logger("test_api", "logs/test_api.log", debug=True)
logger = logger

@classmethod
def setUpClass(cls) -> None:
cls.logger.info("接口测试开始")

@classmethod
def tearDownClass(cls) -> None:
cls.logger.info("接口测试结束")

def test_api_response(self):
self.logger.info("用例开始测试")
# 1、测试数据
for url, method in read_csv():
# 2、测试步骤
result = send_http_request(url=url, method=method)
self.logger.debug("method: {}, url: {}".format(method, url))
# 3、状态码断言
self.assertEqual(result.status_code, 200)
self.logger.info("用例结束测试")

8-3 配置封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
logs:                               # 存储项目日志
- test_log.log # 测试输出日志
- test_api.log
common: # 存储公用方法
- __init__.py # 【✔】实现单例模式,引入配置文件封装后的功能
- log_output.py # 日志功能封装
- http_request.py # 请求功能封装
- config_func.py # 【✔】配置文件封装,函数封装,使用conf.yaml
- config_class.py # 【✔】配置文件封装,类封装,使用conf.ini,与函数封装二选一即可
reports: # 存储项目报告
- test_beautifulreport
test_data: # 存储用例数据
- test_api.csv
test_cases: # 存储测试用例
- test_api.py # 【✔】增加导入模块、修改用例数据路径
- main.py # 【✔】作为入口函数,修改生成报告路径
- conf.ini # 【✔】新建配置文件ini
- conf.yaml # 【✔】新建配置文件yaml

(1) main.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
# main.py,项目入口
import unittest
from common import config
# from XTestRunner import HTMLTestRunner
# from HTMLTestRunner import HTMLTestRunner
from BeautifulReport import BeautifulReport

if __name__ == "__main__":
# 收集所有的用例,并返回测试套件
suite = unittest.TestLoader().discover("test_cases")

# 执行所有的用例,并生成报告
# with open("./reports/test_xtesttunner.html", "wb") as f:
# runner = HTMLTestRunner(
# # verbosity参数:0是简单报告,1是一般报告,2是详细报告
# stream=f, verbosity=2, title="UnitTest测试报告",
# description="UnitTest测试报告:内容描述..."
# )
# result = runner.run(suite)

# with open("./reports/test_htmltestrunner.html", "wb") as f:
# runner = HTMLTestRunner(
# stream=f, verbosity=2, title=u"UnitTest测试报告",
# description=u"UnitTest测试报告:内容描述..."
# )
# result = runner.run(suite)

# BeautifulReport(suite).report(
# # description在BeautifulReport报告中是用例名称
# description="UnitTest接口测试",
# filename="./reports/test_beautifulreport.html"
# )

# 关键在于这里的配置,使用BeautifulReport方法,只生成BeautifulReport报告
BeautifulReport(suite).report(**config["report"])

(2) conf.ini

1
2
3
4
5
6
7
8
9
10
11
12
; conf.ini
[log]
name = pylog
filename = logs/test_api.log
debug = True

[testdata]
file = test_data/test_api.csv

[report]
filename = reports/test_beautifulreport.html
description = UnitTest接口测试

(3) conf.yaml

1
2
3
4
5
6
7
8
9
10
# conf.yaml
log:
name: pylog
filename: logs/test_api.log
debug: True
testdata:
file: test_data/test_api.csv
report:
filename: reports/test_beautifulreport.html
description: UnitTest接口测试

(4) test_api.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
37
38
39
40
41
42
43
44
# test_api.py
import csv
import unittest
from common import logger, config
# from common.log_output import get_logger
from common.http_request import send_http_request


def read_csv():
phone_list = []
# with open("./test_data/test_api.csv", "r", encoding="utf-8") as f:
with open(config["testdata"]["file"], "r", encoding="utf-8") as f:
filename = csv.reader(f)
next(filename, None)
for r in filename:
# 使用strip()方法去除额外的空格
cleaned_data = [item.strip() for item in r]
phone_list.append(cleaned_data)
return phone_list


class TestAPI(unittest.TestCase):
# 将日志相关内容定义为类属性
# logger = get_logger("test_api", "logs/test_api.log", debug=True)
logger = logger

@classmethod
def setUpClass(cls) -> None:
cls.logger.info("接口测试开始")

@classmethod
def tearDownClass(cls) -> None:
cls.logger.info("接口测试结束")

def test_api_response(self):
self.logger.info("用例开始测试")
# 1、测试数据
for url, method in read_csv():
# 2、测试步骤
result = send_http_request(url=url, method=method)
self.logger.debug("method: {}, url: {}".format(method, url))
# 3、状态码断言
self.assertEqual(result.status_code, 200)
self.logger.info("用例结束测试")

(5) __init__.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# __init__.py
# from .config_class import Config
from .log_output import get_logger
from .config_func import get_config

# # 单例模式,避免用例中调用的日志方法或文件,同名时导致重复添加多个日志处理器
# logger = get_logger("test_api", "logs/test_api.log", debug=True)

# 使用config_func获取解析出来的配置数据字典
config = get_config("conf.yaml")
# 将字典中log的数据解包传入
logger = get_logger(**config["log"])
print(logger)

# # 使用config_class获取解析出来的配置数据字典
# config = Config("conf.ini").parse()
# logger = get_logger(**config["log"])
# print(logger)

(6) config_func.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
# config_func.py
import yaml
from configparser import ConfigParser


# 函数封装,定义返回类型为字典
def get_config(filename, encoding="utf-8") -> dict:
"""
获取配置文件
:param filename: 文件名
:param encoding: 文件编码
:return: data
"""
# 获取文件名后缀
suffix = filename.split(".")[-1]
# 判断这个配置文件的类型
if suffix in ["ini", "cfg", "cnf"]:
# ini配置
conf = ConfigParser()
conf.read(filename, encoding=encoding)
# 将ini配置信息解析成一个字典
data = {}
for section in conf.sections():
data[section] = dict(conf.items(section))
elif suffix in ["yaml", "yml"]:
# yaml配置
with open(filename, "r", encoding=encoding) as f:
data = yaml.load(f, Loader=yaml.FullLoader)
else:
raise ValueError("不能识别的配置文件后缀名!")
return data


if __name__ == "__main__":
result = get_config("../conf.yaml")
print(result)

(7) config_class.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# config_class.py
import yaml
from configparser import ConfigParser


# 类封装
class Config:
def __init__(self, filename, encoding="utf-8"):
# 初始化工作
self.filename = filename
self.encoding = encoding
self.suffix = filename.split(".")[-1]
if self.suffix not in ["ini", "conf", "cnf", "cfg", "yml", "yaml"]:
raise ValueError("不能识别的配置文件后缀名!")

def __parse_ini(self):
"""
解析ini、conf、cnf、cfg文件
:return:
"""
conf = ConfigParser()
conf.read(self.filename, encoding=self.encoding)
# 将ini配置信息解析成一个大字典
data = {}
for section in conf.sections():
data[section] = dict(conf.items(section))
return data

def __parse_yaml(self):
"""
解析yaml、yml文件
:return:
"""
with open(self.filename, "r", encoding=self.encoding) as f:
data = yaml.load(f, Loader=yaml.FullLoader)
return data

def parse(self):
"""
解析配置文件
:return:
"""
if self.suffix in ["yaml", "yml"]:
return self.__parse_yaml()
else:
return self.__parse_ini()


if __name__ == "__main__":
result = Config("../conf.yaml")
result = result.parse()
print(result)
result = Config("../conf.ini")
result = result.parse()
print(result)

8-4 变量配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
logs:                               # 存储项目日志
- test_log.log # 测试输出日志
- test_api.log
common: # 存储公用方法
- __init__.py # 【✔】实现单例模式,将settings.LOG_CONFIG的值解包传入
- log_output.py # 日志功能封装
- http_request.py # 请求功能封装
reports: # 存储项目报告
- test_beautifulreport.html
test_data: # 存储用例数据
- test_api.csv
test_cases: # 存储测试用例
- test_api.py # 【✔】增加导入模块、修改用例数据路径
- main.py # 【✔】作为入口函数,修改生成报告路径
- settings.py # 【✔】新建配置文件

(1) main.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
# main.py,项目入口
import unittest
import settings
# from XTestRunner import HTMLTestRunner
# from HTMLTestRunner import HTMLTestRunner
from BeautifulReport import BeautifulReport

if __name__ == "__main__":
# 收集所有的用例,并返回测试套件
suite = unittest.TestLoader().discover("test_cases")

# 执行所有的用例,并生成报告
# with open("./reports/test_xtesttunner.html", "wb") as f:
# runner = HTMLTestRunner(
# # verbosity参数:0是简单报告,1是一般报告,2是详细报告
# stream=f, verbosity=2, title="UnitTest测试报告",
# description="UnitTest测试报告:内容描述..."
# )
# result = runner.run(suite)

# with open("./reports/test_htmltestrunner.html", "wb") as f:
# runner = HTMLTestRunner(
# stream=f, verbosity=2, title=u"UnitTest测试报告",
# description=u"UnitTest测试报告:内容描述..."
# )
# result = runner.run(suite)

# BeautifulReport(suite).report(
# # description在BeautifulReport报告中是用例名称
# description="UnitTest测试报告",
# filename="./reports/test_beautifulreport.html"
# )

# 关键在于这里的配置,使用BeautifulReport方法,只生成BeautifulReport报告
# BeautifulReport(suite).report(**config["report"])
BeautifulReport(suite).report(**settings.REPORT_CONFIG)

(2) settings.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 日志配置
LOG_CONFIG = {
"name": "pylog",
"filename": "logs/test_api.log",
"debug": True
}

# 测试数据配置
TEST_DATA_FILE = "test_data/test_api.csv"

# 测试报告
REPORT_CONFIG = {
"filename": "reports/test_beautifulreport.html",
"description": "UnitTest接口测试"
}

(3) test_api.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
37
38
39
40
41
42
43
44
45
# test_api.py
import csv
import unittest
import settings
from common import logger
# from common.log_output import get_logger
from common.http_request import send_http_request


def read_csv():
phone_list = []
# with open("./test_data/test_api.csv", "r", encoding="utf-8") as f:
with open(settings.TEST_DATA_FILE, "r", encoding="utf-8") as f:
filename = csv.reader(f)
next(filename, None)
for r in filename:
# 使用strip()方法去除额外的空格
cleaned_data = [item.strip() for item in r]
phone_list.append(cleaned_data)
return phone_list


class TestAPI(unittest.TestCase):
# 将日志相关内容定义为类属性
# logger = get_logger("test_api", "logs/test_api.log", debug=True)
logger = logger

@classmethod
def setUpClass(cls) -> None:
cls.logger.info("接口测试开始")

@classmethod
def tearDownClass(cls) -> None:
cls.logger.info("接口测试结束")

def test_api_response(self):
self.logger.info("用例开始测试")
# 1、测试数据
for url, method in read_csv():
# 2、测试步骤
result = send_http_request(url=url, method=method)
self.logger.debug("method: {}, url: {}".format(method, url))
# 3、状态码断言
self.assertEqual(result.status_code, 200)
self.logger.info("用例结束测试")

(4) __init__.py

1
2
3
4
5
6
7
8
9
# __init__.py
import settings
from .log_output import get_logger

# # 单例模式,避免用例中调用的日志方法或文件,同名时导致重复添加多个日志处理器
# logger = get_logger("test_api", "logs/test_api.log", debug=True)

# 将settings.LOG_CONFIG的值解包传入
logger = get_logger(**settings.LOG_CONFIG)

UnitTest 测试框架
https://stitch-top.github.io/2023/07/23/ce-shi-kuang-jia/tf04-unittest-ce-shi-kuang-jia/
作者
Dr.626
发布于
2023年7月23日 21:11:00
许可协议