unittest测试模块:Python 标准库中的单元测试利器

发布于:2025-04-08 ⋅ 阅读:(14) ⋅ 点赞:(0)

在当今的软件开发中,测试的必要性不言而喻。为了确保代码的质量和稳定性,开发者需要一种高效的方式去编写和运行单元测试。Python 提供了一个强大的工具——unittest。这是一个标准库模块,专为编写和运行测试而设计,帮助开发者减少错误并提高代码的可维护性。

为什么选择 unittest?

  1. 内置支持: 作为标准库的一部分,unittest 与 Python 语言的兼容性极高,无需额外安装。
  2. 功能强大: 提供丰富的断言方法、测试夹具和错误报告,方便调试。
  3. 易于使用: 结构清晰,便于组织和运行测试。

如何使用unittest

使用 unittest 进行单元测试的基本步骤如下:

  1. 导入 unittest 模块
  2. 创建测试类,并让其继承自 unittest.TestCase
  3. 编写测试方法,方法名需以 test_ 开头。
  4. 使用断言方法,确保输出结果与预期一致。
  5. 运行测试

安装和导入

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()

 

解释

  1. 导入模块: 导入 unittest 模块和需要测试的函数 add
  2. 定义测试类TestAddFunction 继承自 unittest.TestCase
  3. 编写测试方法: 每个方法名以 test_ 开头,用于标识这是一个测试方法。
  4. 使用断言方法: 使用 assertEqual 方法来验证 add 函数的输出是否符合预期。
  5. 运行测试: 使用 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 来提升代码质量的兴趣!


如果你还有其他主题或具体方向想要探讨,随时告诉我!


网站公告

今日签到

点亮在社区的每一天
去签到