在当今的软件开发中,测试的必要性不言而喻。为了确保代码的质量和稳定性,开发者需要一种高效的方式去编写和运行单元测试。Python 提供了一个强大的工具——unittest
。这是一个标准库模块,专为编写和运行测试而设计,帮助开发者减少错误并提高代码的可维护性。
为什么选择 unittest?
- 内置支持: 作为标准库的一部分,
unittest
与 Python 语言的兼容性极高,无需额外安装。 - 功能强大: 提供丰富的断言方法、测试夹具和错误报告,方便调试。
- 易于使用: 结构清晰,便于组织和运行测试。
如何使用unittest
使用 unittest
进行单元测试的基本步骤如下:
- 导入
unittest
模块。 - 创建测试类,并让其继承自
unittest.TestCase
。 - 编写测试方法,方法名需以
test_
开头。 - 使用断言方法,确保输出结果与预期一致。
- 运行测试。
安装和导入
unittest
是 Python 标准库的一部分,所以不需要额外安装,可以直接导入并使用它。
import unittest
示例代码
假设我们有一个简单的函数 add
,我们需要编写测试用例来验证它的正确性。文件存盘为my_math.py:
# my_math.py
def add(a, b):
return a + b
现在我们编写测试用例,文件存盘为test_my_math.py:
# test_my_math.py
import unittest
from my_math import add
class TestAddFunction(unittest.TestCase):
def test_add_positive_numbers(self):
self.assertEqual(add(1, 2), 3)
def test_add_negative_numbers(self):
self.assertEqual(add(-1, -2), -3)
def test_add_mixed_numbers(self):
self.assertEqual(add(-1, 2), 1)
def test_add_zero(self):
self.assertEqual(add(0, 0), 0)
def test_add_floats(self):
self.assertEqual(add(1.5, 2.5), 4.0)
if __name__ == '__main__':
unittest.main()
解释
- 导入模块: 导入
unittest
模块和需要测试的函数add
。 - 定义测试类:
TestAddFunction
继承自unittest.TestCase
。 - 编写测试方法: 每个方法名以
test_
开头,用于标识这是一个测试方法。 - 使用断言方法: 使用
assertEqual
方法来验证add
函数的输出是否符合预期。 - 运行测试: 使用
unittest.main()
来运行测试。
运行测试
可以通过以下命令运行测试:
python -m unittest test_my_math.py
或者直接运行包含测试代码的文件:
python test_my_math.py
输出结果
运行测试后,会看到类似以下的输出:
....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
这表示所有测试都通过了。如果有测试失败,会看到具体的错误信息。
其它常用断言方法
assertTrue(expr)
: 确保表达式为True
。assertFalse(expr)
: 确保表达式为False
。assertIsNone(expr)
: 确保表达式为None
。assertIsNotNone(expr)
: 确保表达式不为None
。assertIs(expr1, expr2)
: 确保两个表达式相等。assertIsNot(expr1, expr2)
: 确保两个表达式不相等。assertIn(member, container)
: 确保成员在容器中。assertNotIn(member, container)
: 确保成员不在容器中。assertIsInstance(obj, cls)
: 确保对象是某个类的实例。assertNotIsInstance(obj, cls)
: 确保对象不是某个类的实例。
通过这些基本的步骤和方法,我们就可以编写和运行高效的单元测试来确保自己的代码质量。
CNPL解释器的测试:
编辑文件cnpl_interpreter.py
"""
中文同像性编程语言(CNPL)解释器
基于主谓宾结构和同像性数据结构的编程语言解释器
"""
import re
import math
import json
class CNPLParser:
def __init__(self):
self.ast = {}
def parse(self, tokens):
"""将词法单元转换为抽象语法树"""
if not tokens:
return None
# 简单的主谓宾结构解析
if len(tokens) >= 3 and tokens[1]['type'] == 'operator':
return {
'type': 'operation',
'subject': tokens[0]['value'],
'predicate': tokens[1]['value'],
'object': ' '.join([t['value'] for t in tokens[2:]]) if len(tokens) > 2 else None
}
# 处理单操作数函数
if len(tokens) == 2 and tokens[1]['type'] == 'operator' and tokens[1]['value'] in ["开方", "正弦", "余弦", "正切"]:
return {
'type': 'unary_operation',
'operator': tokens[1]['value'],
'operand': tokens[0]['value']
}
# 默认返回原始token列表
return tokens
class CNPLInterpreter:
def __init__(self):
# 词法单元类型定义
self.token_types = {
"keywords": ["如果", "否则", "循环", "返回", "抛出", "导入", "从", "函数", "变量", "当", "每次"],
"operators": ["加", "减", "乘", "除", "幂", "模", "开方", "正弦", "余弦", "正切"],
"delimiters": ["(", ")", "{", "}", "[", "]", ",", ";", ":"],
"identifier": r'^[\u4e00-\u9fa5a-zA-Z_][\u4e00-\u9fa5a-zA-Z0-9_]*$',
"number": r'^[0-9]+(\.[0-9]+)?$'
}
# 变量表
self.variables = {}
# 语法分析器
self.parser = CNPLParser()
def lexer(self, input_str):
"""词法分析器"""
tokens = []
pos = 0
while pos < len(input_str):
# 跳过空白字符
match = re.match(r'\s+', input_str[pos:])
if match:
pos += len(match.group())
continue
if pos >= len(input_str):
break
# 检查关键字、操作符和分隔符
found = False
for token_type in ["keywords", "operators", "delimiters"]:
for token in self.token_types[token_type]:
if input_str[pos:].startswith(token):
tokens.append({"type": token_type[:-1], "value": token})
pos += len(token)
found = True
break
if found:
break
if found:
continue
# 检查数字(优先检查数字,因为数学函数可能包含数字)
match = re.match(self.token_types["number"], input_str[pos:])
if match:
token = match.group()
tokens.append({"type": "number", "value": token})
pos += len(token)
continue
# 检查数学函数(如'开方'等)
for op in self.token_types["operators"]:
if input_str[pos:].startswith(op):
tokens.append({"type": "operator", "value": op})
pos += len(op)
found = True
break
if found:
continue
# 检查标识符
match = re.match(self.token_types["identifier"], input_str[pos:])
if match:
token = match.group()
tokens.append({"type": "identifier", "value": token})
pos += len(token)
continue
raise Exception(f"无法识别的字符: {input_str[pos]}")
return tokens
def parse_svo(self, code):
"""解析主谓宾结构"""
parts = code.split(" ")
# 处理单操作数的数学函数
if len(parts) == 2 and parts[1] in ["开方", "正弦", "余弦", "正切"]:
# 先检查操作数是否为数字
try:
operand = float(parts[0])
result = self.execute_operation(parts[0], parts[1], "")
if isinstance(result, (int, float)):
return float(result)
elif isinstance(result, str):
try:
return float(result)
except ValueError:
raise Exception("数学函数结果必须是数字")
elif isinstance(result, bool):
return float(result)
elif result is None:
return 0.0
else:
raise Exception(f"无效的数学函数结果类型: {type(result)}")
except ValueError:
raise Exception("数学函数的操作数必须是数字")
if len(parts) < 2:
raise Exception("无效的主谓宾结构")
subject = parts[0]
predicate = parts[1]
object_ = " ".join(parts[2:])
return self.execute_operation(subject, predicate, object_)
def execute_operation(self, subject, predicate, object_):
"""执行基本操作"""
# 处理条件判断
if predicate == "如果":
try:
condition = self.interpret(subject)
if isinstance(condition, bool):
if condition:
return self.interpret(object_)
return None
elif isinstance(condition, (int, float)):
if condition != 0:
return self.interpret(object_)
return None
else:
raise Exception(f"条件表达式必须返回布尔值或数字,得到: {type(condition)}")
except Exception as e:
raise Exception(f"条件判断执行错误: {str(e)}")
# 处理循环结构
if predicate == "循环" or predicate == "当":
try:
result = None
while True:
condition = self.interpret(subject)
if isinstance(condition, bool):
if not condition:
break
elif isinstance(condition, (int, float)):
if condition == 0:
break
else:
raise Exception(f"循环条件必须返回布尔值或数字,得到: {type(condition)}")
result = self.interpret(object_)
return result
except Exception as e:
raise Exception(f"循环执行错误: {str(e)}")
# 处理单操作数函数
if predicate in ["开方", "正弦", "余弦", "正切"]:
print(f"====执行单操作数函数: {predicate}({subject})")
try:
subj_num = float(subject)
print(f"====操作数转换成功: {subject} -> {subj_num}")
result = None
if predicate == "开方":
result = math.sqrt(subj_num)
print(f"====开方结果: {result}")
elif predicate == "正弦":
result = math.sin(subj_num)
print(f"====正弦结果: {result}")
elif predicate == "余弦":
result = math.cos(subj_num)
print(f"====余弦结果: {result}")
elif predicate == "正切":
result = math.tan(subj_num)
print(f"====正切结果: {result}")
# 确保返回数值类型
if isinstance(result, (int, float)):
return float(result)
elif isinstance(result, str):
try:
return float(result)
except ValueError:
raise Exception("数学函数结果必须是数字")
elif isinstance(result, bool):
return float(result)
elif result is None:
return 0.0
else:
raise Exception(f"无效的数学函数结果类型: {type(result)}")
except ValueError as e:
print(f"====操作数转换失败: {e}")
raise Exception("操作数必须是数字")
# 转换为数字
try:
subj_num = float(subject)
obj_num = float(object_)
except ValueError:
raise Exception("操作数必须是数字")
# 执行操作
if predicate == "加":
return subj_num + obj_num
elif predicate == "减":
return subj_num - obj_num
elif predicate == "乘":
return subj_num * obj_num
elif predicate == "除":
return subj_num / obj_num
elif predicate == "幂":
return subj_num ** obj_num
elif predicate == "模":
return subj_num % obj_num
else:
raise Exception(f"未实现的操作: {predicate}")
def execute_homiconic_code(self, code_obj):
"""执行同像性代码"""
if code_obj["type"] == "代码块":
return self.interpret(code_obj["content"])
elif code_obj["type"] == "变量":
return self.get_variable(code_obj["name"])
else:
raise Exception(f"未知的代码类型: {code_obj['type']}")
def set_variable(self, name, value):
"""设置变量"""
self.variables[name] = value
def get_variable(self, name):
"""获取变量"""
if name in self.variables:
return self.variables[name]
raise Exception(f"未定义的变量: {name}")
def interpret(self, code_block):
"""主解释函数"""
try:
if isinstance(code_block, str):
# 处理数学运算表达式
if re.match(r'^\d+\s+[加减乘除幂模开方正弦余弦正切]\s+\d+$', code_block) or \
re.match(r'^\d+\s+[开方正弦余弦正切]$', code_block):
result = self.parse_svo(code_block)
print(f"==处理数学运算表达式 code_block:{code_block} 解析结果result: {result} type is:{type(result)}")
if isinstance(result, (int, float)):
return result
try:
return float(result)
except ValueError:
if isinstance(result, str) and result.replace('.', '', 1).isdigit():
return float(result)
raise Exception(f"无法转换为数字的结果: {result}")
# 确保数学运算结果返回数值类型
if isinstance(result, str) and result.replace('.', '', 1).isdigit():
return float(result)
elif isinstance(result, (int, float)):
return result
elif isinstance(result, bool):
return float(result)
elif result is None:
return 0.0
else:
raise Exception(f"无法转换为数字的结果: {result}")
# 处理纯数字
if re.match(r'^\d+$', code_block):
return float(code_block)
# 先进行词法分析
tokens = self.lexer(code_block)
# 生成AST
ast = self.parser.parse(tokens)
# 如果是操作表达式
if isinstance(ast, dict) and ast.get('type') in ['operation', 'unary_operation']:
return self.execute_ast(ast)
# 否则回退到原始解析方式
return self.parse_svo(code_block)
elif isinstance(code_block, dict):
return self.execute_homiconic_code(code_block)
else:
raise Exception("不支持的代码块类型")
except Exception as e:
return f"错误: {str(e)}"
def execute_ast(self, ast):
"""执行AST节点"""
if ast['type'] == 'operation':
# 处理变量引用
subject = self.get_variable(ast['subject']) if ast['subject'] in self.variables else ast['subject']
object_ = self.get_variable(ast['object']) if ast['object'] in self.variables else ast['object']
return self.execute_operation(subject, ast['predicate'], object_)
elif ast['type'] == 'unary_operation':
operand = self.get_variable(ast['operand']) if ast['operand'] in self.variables else ast['operand']
return self.execute_operation(operand, ast['operator'], "")
# 测试代码
if __name__ == "__main__":
interpreter = CNPLInterpreter()
# 测试基本数学运算
print("5加3的结果是:", interpreter.interpret("5 加 3"))
print("10减2的结果是:", interpreter.interpret("10 减 2"))
print("4乘2的结果是:", interpreter.interpret("4 乘 2"))
print("8除2的结果是:", interpreter.interpret("8 除 2"))
# 测试同像性数据结构
code_obj = {
"type": "代码块",
"content": "5 加 3"
}
print("同像性代码执行结果:", interpreter.interpret(code_obj))
编辑文件测试文件test_parser.py
"""
CNPL解释器测试文件
测试词法分析、语法解析和执行功能
"""
import unittest
from cnpl_interpreter import CNPLInterpreter
class TestCNPLInterpreter(unittest.TestCase):
def setUp(self):
self.interpreter = CNPLInterpreter()
def test_basic_math_operations(self):
"""测试基本数学运算"""
# self.assertEqual(self.interpreter.interpret("5 加 3"), 8)
# self.assertEqual(self.interpreter.interpret("10 减 2"), 8)
# self.assertEqual(self.interpreter.interpret("4 乘 2"), 8)
# self.assertEqual(self.interpreter.interpret("8 除 2"), 4)
# self.assertEqual(self.interpreter.interpret("2 幂 3"), 8)
# self.assertEqual(self.interpreter.interpret("10 模 3"), 1)
def test_math_functions(self):
"""测试数学函数"""
x = self.interpreter.interpret("4 开方")
print(f"====x is {x}, x.type:{type(x)}")
self.assertAlmostEqual(self.interpreter.interpret("4 开方"), 2.0)
self.assertAlmostEqual(self.interpreter.interpret("0 正弦"), 0.0)
self.assertAlmostEqual(self.interpreter.interpret("0 余弦"), 1.0)
self.assertAlmostEqual(self.interpreter.interpret("0 正切"), 0.0)
def test_variables(self):
"""测试变量操作"""
self.interpreter.set_variable("变量一", 5)
self.assertEqual(self.interpreter.get_variable("变量一"), 5)
code_obj = {"type": "变量", "name": "变量一"}
self.assertEqual(self.interpreter.interpret(code_obj), 5)
def test_homiconic_structures(self):
"""测试同像性数据结构"""
code_obj = {
"type": "代码块",
"content": "5 加 3"
}
self.assertEqual(self.interpreter.interpret(code_obj), 8)
if __name__ == "__main__":
unittest.main()
执行测试:
python test_parser.py
当然现在还没完全调试好,所以测试有报错:
python test_parser.py
.==处理数学运算表达式 code_block:5 加 3 解析结果result: 8.0 type is:<class 'float'>
.====x is 错误: 无法识别的字符: 4, x.type:<class 'str'>
E.
======================================================================
ERROR: test_math_functions (__main__.TestCNPLInterpreter)
测试数学函数
----------------------------------------------------------------------
Traceback (most recent call last):
File "E:\work\cntrae\test_parser.py", line 26, in test_math_functions
self.assertAlmostEqual(self.interpreter.interpret("4 开方"), 2.0)
File "e:\py310\lib\unittest\case.py", line 876, in assertAlmostEqual
diff = abs(first - second)
TypeError: unsupported operand type(s) for -: 'str' and 'float'
----------------------------------------------------------------------
Ran 4 tests in 0.009s
FAILED (errors=1)
总结:
通过使用 unittest
模块,Python 开发者可以有效地为他们的代码编写测试,确保功能实现的正确性和代码的健壮性。无论是初学者还是经验丰富的开发者,掌握 unittest
都能为软件开发工作带来极大的帮助。希望本文能够激发你使用 unittest
来提升代码质量的兴趣!
如果你还有其他主题或具体方向想要探讨,随时告诉我!