在芯片验证的传统实践中,UVM等框架被广泛采用。尽管它们提供了一整套验证方法,但通常只适用于特定的硬件描述语言和仿真环境。本工具突破了这些限制,能够将仿真代码转换成C++或Python,使得我们可以利用软件验证工具来进行更全面的测试。
因为Python具有强大的生态系统,所以本项目主要以Python作为示例,简单介绍Pytest和Hypothesis两个经典软件测试框架。Pytest以其简洁的语法和丰富的功能,轻松应对各种测试需求。而Hypothesis则通过生成测试用例,揭示出意料之外的边缘情况,提高了测试的全面性和深度。
我们的项目从一开始就设计为与多种现代软件测试框架兼容。我们鼓励您探索这些工具的潜力,并将其应用于您的测试流程中。通过亲身实践,您将更深刻地理解这些工具如何提升代码的质量和可靠性。让我们一起努力,提高芯片开发的质量。
集成测试框架
- 1: PyTest
- 2: Hypothesis
1 - PyTest
软件测试
在正式开始pytest 之间我们先了解一下软件的测试,软件测试一般分为如下四个方面
- 单元测试:称模块测试,针对软件设计中的最小单位——程序模块,进行正确性检查的测试工作
- 集成测试:称组装测试,通常在单元测试的基础上,将所有程序模块进行有序的、递增测试,重点测试不同模块的接口部分
- 系统测试:将整个软件系统看成一个整体进行测试,包括对功能、性能以及软件所运行的软硬件环境进行测试
- 验收测试:指按照项目任务书或合同、供需双方约定的验收依据文档进行的对整个系统的测试与评审,决定是否接收或拒收系统
pytest最初是作为一个单元测试框架而设计的,但它也提供了许多功能,使其能够进行更广泛的测试,包括集成测试,系统测试,他是一个非常成熟的全功能的python 测试框架。 它通过收集测试函数和模块,并提供丰富的断言库来简化测试的编写和运行,是一个非常成熟且功能强大的 Python 测试框架,具有以下几个特点:
- 简单灵活:Pytest 容易上手,且具有灵活性。
- 支持参数化:您可以轻松地为测试用例提供不同的参数。
- 全功能:Pytest 不仅支持简单的单元测试,还可以处理复杂的功能测试。您甚至可以使用它来进行自动化测试,如 Selenium 或 Appium 测试,以及接口自动化测试(结合 Pytest 和 Requests 库)。
- 丰富的插件生态:Pytest 有许多第三方插件,您还可以自定义扩展。一些常用的插件包括:
pytest-selenium
:集成 Selenium。pytest-html
:生成HTML测试报告。pytest-rerunfailures
:在失败的情况下重复执行测试用例。pytest-xdist
:支持多 CPU 分发。
- 与 Jenkins 集成良好。
- 支持 Allure 报告框架。
本文将基于测试需求简单介绍pytest的用法,其完整手册在这里,供同学们进行深入学习。
Pytest安装
# 安装pytest:
pip install pytest
# 升级pytest
pip install -U pytest
# 查看pytest版本
pytest --version
# 查看已安装包列表
pip list
# 查看pytest帮助文档
pytest -h
# 安装第三方插件
pip install pytest-sugar
pip install pytest-rerunfailures
pip install pytest-xdist
pip install pytest-assume
pip install pytest-html
Pytest使用
命名规则
# 首先在使用pytest 时我们的模块名通常是以test开头或者test结尾,也可以修改配置文件,自定义命名规则
# test_*.py 或 *_test.py
test_demo1
demo2_test
# 模块中的类名要以Test 开始且不能有init 方法
class TestDemo1:
class TestLogin:
# 类中定义的测试方法名要以test_开头
test_demo1(self)
test_demo2(self)
# 测试用例
class test_one:
def test_demo1(self):
print("测试用例1")
def test_demo2(self):
print("测试用例2")
Pytest 参数
pytest支持很多参数,可以通过help命令查看
pytest -help
我们在这里列出来常用的几个:
-m: 用表达式指定多个标记名。 pytest 提供了一个装饰器 @pytest.mark.xxx,用于标记测试并分组(xxx是你定义的分组名),以便你快速选中并运行,各个分组直接用 and、or 来分割。
-v: 运行时输出更详细的用例执行信息 不使用-v参数,运行时不会显示运行的具体测试用例名称;使用-v参数,会在 console 里打印出具体哪条测试用例被运行。
-q: 类似 unittest 里的 verbosity,用来简化运行输出信息。 使用 -q 运行测试用例,仅仅显示很简单的运行信息, 例如:
.s.. [100%]
3 passed, 1 skipped in 9.60s
-k: 可以通过表达式运行指定的测试用例。 它是一种模糊匹配,用 and 或 or 区分各个关键字,匹配范围有文件名、类名、函数名。
-x: 出现一条测试用例失败就退出测试。 在调试时,这个功能非常有用。当出现测试失败时,停止运行后续的测试。
-s: 显示print内容 在运行测试脚本时,为了调试或打印一些内容,我们会在代码中加一些print内容,但是在运行pytest时,这些内容不会显示出来。如果带上-s,就可以显示了。
pytest test_se.py -s
Pytest 选择测试用例执行
在 Pytest 中,您可以按照测试文件夹、测试文件、测试类和测试方法的不同维度来选择执行测试用例。
- 按照测试文件夹执行
# 执行所有当前文件夹及子文件夹下的所有测试用例
pytest .
# 执行跟当前文件夹同级的tests文件夹及子文件夹下的所有测试用例
pytest ../tests
# 按照测试文件执行
# 运行test_se.py下的所有的测试用例
pytest test_se.py
# 按照测试类执行,必须以如下格式:
pytest 文件名 .py:: 测试类,其中“::”是分隔符,用于分割测试module和测试类。
# 运行test_se.py文件下的,类名是TestSE下的所有测试用例
pytest test_se.py::TestSE
# 测试方法执行,必须以如下格式:
pytest 文件名 .py:: 测试类 :: 测试方法,其中 “::” 是分隔符,用于分割测试module、测试类,以及测试方法。
# 运行test_se.py文件下的,类名是TestSE下的,名字为test_get_new_message的测试用例
pytest test_se.py::TestSE::test_get_new_message
# 以上选择测试用例的方法均是在**命令行**,如果您想直接在测试程序里执行可以直接在main函数中**调用pytest.main()**,其格式为:
pytest.main([模块.py::类::方法])
此外,Pytest 还支持控制测试用例执行的多种方式,例如过滤执行、多进程运行、重试运行等。
使用Pytest编写验证
- 在测试过程中,我们使用之前验证过的加法器,进入Adder文件夹,在picker_out_adder目录下新建一个test_adder.py文件,内容如下:
# 导入测试模块和所需的库
from UT_Adder import *
import pytest
import ctypes
import random
# 使用 pytest fixture 来初始化和清理资源
@pytest.fixture
def adder():
# 创建 DUTAdder 实例,加载动态链接库
dut = DUTAdder()
# 执行一次时钟步进,准备 DUT
dut.Step(1)
# yield 语句之后的代码会在测试结束后执行,用于清理资源
yield dut
# 清理DUT资源,并生成测试覆盖率报告和波形
dut.Finish()
class TestFullAdder:
# 将 full_adder 定义为静态方法,因为它不依赖于类实例
@staticmethod
def full_adder(a, b, cin):
cin = cin & 0b1
Sum = ctypes.c_uint64(a).value
Sum += ctypes.c_uint64(b).value + cin
Cout = (Sum >> 64) & 0b1
Sum &= 0xffffffffffffffff
return Sum, Cout
# 使用 pytest.mark.usefixtures 装饰器指定使用的 fixture
@pytest.mark.usefixtures("adder")
# 定义测试方法,adder 参数由 pytest 通过 fixture 注入
def test_adder(self, adder):
# 进行多次随机测试
for _ in range(114514):
# 随机生成 64 位的 a 和 b,以及 1 位的进位 cin
a = random.getrandbits(64)
b = random.getrandbits(64)
cin = random.getrandbits(1)
# 设置 DUT 的输入
adder.a.value = a
adder.b.value = b
adder.cin.value = cin
# 执行一次时钟步进
adder.Step(1)
# 使用静态方法计算预期结果
sum, cout = self.full_adder(a, b, cin)
# 断言 DUT 的输出与预期结果相同
assert sum == adder.sum.value
assert cout == adder.cout.value
if __name__ == "__main__":
pytest.main(['-v', 'test_adder.py::TestFullAdder'])
- 运行测试之后输出如下:
collected 1 item
test_adder.py ✓ 100% ██████████
Results (4.33s):
测试成功表明,在经过114514次循环之后,我们的设备暂时没有发现bug。然而,使用多次循环的随机数生成测试用例会消耗大量资源,并且这些随机生成的测试用例可能无法有效覆盖所有边界条件。在下一部分,我们将介绍一种更有效的测试用例生成方法。
2 - Hypothesis
Hypothesis
在上一节中,我们通过手动编写测试用例,并为每个用例指定输入和预期输出。这种方式存在一些问题,例如测试用例覆盖不全面、边界条件 容易被忽略等。它是一个用于属性基于断言的软件测试的 Python 库。Hypothesis 的主要目标是使测试更简单、更快速、更可靠。它使用了一种称为属性基于断言的测试方法,即你可以为你的代码编写一些假(hypotheses),然后 Hypothesis 将会自动生成测试用例并验证这些假设。这使得编写全面且高效的测试变得更加容易。Hypothesis 可以自动生成各种类型的输入数据,包括基本类型(例如整数、浮点数、字符串等)、容器类型(例如列表、集合、字典等)、自定义类型等。然后,它会根据你提供的属性(即断言)进行测试,如果发现测试失败,它将尝试缩小输入数据的范围以找出最小的失败案例。通过 Hypothesis,你可以更好地覆盖代码的边界条件,并发现那些你可能没有考虑到的错误情况。这有助于提高代码的质量和可靠性。
基本概念
- 测试函数:即待测试的函数或方法,我们需要对其进行测试。
- 属性:定义了测试函数应该满足的条件。属性是以装饰器的形式应用于测试函数上的。
- 策略:用于生成测试数据的生成器。Hypothesis 提供了一系列内置的策略,如整数、字符串、列表等。我们也可以自定义策略。
- 测试生成器:基于策略生成测试数据的函数。Hypothesis 会自动为我们生成测试数据,并将其作为参数传递给测试函数。
本文将基于测试需求简单介绍Hypothesis的用法,其完整手册在这里,供同学们进行深入学习。
安装
使用pip安装,在python中导入即可使用
pip install hypothesis
import hypothesis
基本用法
属性和策略
Hypothesis 使用属性装饰器来定义测试函数的属性。最常用的装饰器是 @given,它指定了测试函数应该满足的属性。
我们可以通过@given 装饰器定义了一个测试函数 test_addition。并给x 添加对应的属性,测试生成器会自动为测试函数生成测试数据,并将其作为参数传递给函数,例如
def addition(number: int) -> int:
return number + 1
@given(x=integers(), y=integers())
def test_addition(x, y):
assert x + 1 == addition(1)
其中integers () 是一个内置的策略,用于生成整数类型的测试数据。Hypothesis 提供了丰富的内置策略,用于生成各种类型的测试数据。除了integers ()之外,还有字符串、布尔值、列表、字典等策略。例如使用 text () 策略生成字符串类型的测试数据,使用 lists (text ()) 策略生成字符串列表类型的测试数据
@given(s=text(), l=lists(text()))
def test_string_concatenation(s, l):
result = s + "".join(l)
assert len(result) == len(s) + sum(len(x) for x in l)
除了可以使用内置的策略以外,还可以使用自定义策略来生成特定类型的测试数据,例如我们可以生产一个非负整形的策略
def non_negative_integers():
return integers(min_value=0)
@given(x=non_negative_integers())
def test_positive_addition(x):
assert x + 1 > x
期望
我们可以通过expect 来指明需要的函数期待得到的结果
@given(x=integers())
def test_addition(x):
expected = x + 1
actual = addition(x)
假设和断言
在使用 Hypothesis 进行测试时,我们可以使用标准的 Python 断言来验证测试函数的属性。Hypothesis 会自动为我们生成测试数据,并根据属性装饰器中定义的属性来运行测试函数。如果断言失败,Hypothesis 会尝试缩小测试数据的范围,以找出导致失败的最小样例。
假如我们有一个字符串反转函数,我们可以通过assert 来判断翻转两次后他是不是等于自身
def test_reverse_string(s):
expected = x + 1
actual = addition(x)
assert actual == expected
编写测试
-
Hypothesis 中的测试由两部分组成:一个看起来像您选择的测试框架中的常规测试但带有一些附加参数的函数,以及一个@given指定如何提供这些参数的装饰器。以下是如何使用它来验证我们之前验证过的全加器的示例:
-
在上一节的代码基础上,我们进行一些修改,将生成测试用例的方法从随机数修改为integers ()方法,修改后的代码如下:
from Adder import *
import pytest
from hypothesis import given, strategies as st
# 使用 pytest fixture 来初始化和清理资源
@pytest.fixture(scope="class")
def adder():
# 创建 DUTAdder 实例,加载动态链接库
dut = DUTAdder()
# yield 语句之后的代码会在测试结束后执行,用于清理资源
yield dut
# 清理DUT资源,并生成测试覆盖率报告和波形
dut.Finish()
class TestFullAdder:
# 将 full_adder 定义为静态方法,因为它不依赖于类实例
@staticmethod
def full_adder(a, b, cin):
cin = cin & 0b1
Sum = a
Sum += b + cin
Cout = (Sum >> 128) & 0b1
Sum &= 0xffffffffffffffffffffffffffffffff
return Sum, Cout
# 使用 hypothesis 自动生成测试用例
@given(
a=st.integers(min_value=0, max_value=0xffffffffffffffff),
b=st.integers(min_value=0, max_value=0xffffffffffffffff),
cin=st.integers(min_value=0, max_value=1)
)
# 定义测试方法,adder 参数由 pytest 通过 fixture 注入
def test_full_adder_with_hypothesis(self, adder, a, b, cin):
# 计算预期的和与进位
sum_expected, cout_expected = self.full_adder(a, b, cin)
# 设置 DUT 的输入
adder.a.value = a
adder.b.value = b
adder.cin.value = cin
# 执行一次时钟步进
adder.Step(1)
# 断言 DUT 的输出与预期结果相同
assert sum_expected == adder.sum.value
assert cout_expected == adder.cout.value
if __name__ == "__main__":
# 以详细模式运行指定的测试
pytest.main(['-v', 'test_adder.py::TestFullAdder'])
这个例子中,@given 装饰器和 strategies 用于生成符合条件的随机数据。st.integers() 是生成指定范围整数的策略,用于为 a 和 b 生成 0 到 0xffffffffffffffff 之间的数,以及为 cin 生成 0 或 1。Hypothesis会自动重复运行这个测试,每次都使用不同的随机输入,这有助于揭示潜在的边界条件或异常情况。
- 运行测试,输出结果如下:
collected 1 item
test_adder.py ✓ 100% ██████████
Results (0.42s):
1 passed
可以看到在很短的时间里我们已经完成了测试