原理
实现方式
应用场景及常见装饰器
- 在Python接口自动化测试中,装饰器可以用于增强测试函数的功能或改变其行为。以下是一些可能使用的装饰器示例:
设置和清理环境【setup和teardown】
@setup:用于在测试开始前初始化环境或配置。
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
| class CustomTestRunner: def __init__(self): self.setup_done = False def setup(self): print("Setting up environment...") self.setup_done = True def teardown(self): if self.setup_done: print("Tearing down environment...") def setup(func): def wrapper(test_runner, *args, **kwargs): if not test_runner.setup_done: test_runner.setup() return func(test_runner, *args, **kwargs) return wrapper class MyTestCase: def __init__(self): self.test_runner = CustomTestRunner() @setup def test_my_api(self): assert self.test_runner.setup_done, "Setup should have been called"
test_case = MyTestCase() test_case.test_my_api()
|
在这个例子中,我们创建了一个名为CustomTestRunner的类,其中包含了一个setup方法用于初始化环境或配置。我们还定义了一个名为setup的装饰器,它会在调用被装饰的测试函数之前检查是否已经完成了设置,并在必要时调用setup方法。
在MyTestCase类中,我们使用@setup装饰器装饰了test_my_api方法。当我们创建一个MyTestCase实例并调用其test_my_api方法时,装饰器会确保在测试开始前调用了setup方法。
请注意,这个示例使用了一个自定义的测试运行器类(CustomTestRunner)和装饰器(@setup)。在实际项目中,你可能需要根据所使用的测试框架(如unittest、pytest等)来调整实现方式。例如,在unittest框架中,可以使用setUp和tearDown方法代替自定义的setup和teardown方法。
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
| class CustomTestRunner: def __init__(self): self.setup_done = False def setup(self): print("Setting up environment...") self.setup_done = True def teardown(self): if self.setup_done: print("Tearing down environment...") def teardown(func): def wrapper(test_runner, *args, **kwargs): result = func(test_runner, *args, **kwargs) test_runner.teardown() return result return wrapper class MyTestCase: def __init__(self): self.test_runner = CustomTestRunner() @teardown def test_my_api(self): assert self.test_runner.setup_done, "Setup should have been called"
test_case = MyTestCase() test_case.test_my_api()
|
在这个例子中,我们创建了一个名为CustomTestRunner的类,其中包含了一个teardown方法用于清理环境或资源。我们还定义了一个名为teardown的装饰器,它会在被装饰的测试函数执行完毕后调用teardown方法。
在MyTestCase类中,我们使用@teardown装饰器装饰了test_my_api方法。当我们创建一个MyTestCase实例并调用其test_my_api方法时,装饰器会确保在测试结束后调用了teardown方法。
请注意,这个示例使用了一个自定义的测试运行器类(CustomTestRunner)和装饰器(@teardown)。在实际项目中,你可能需要根据所使用的测试框架(如unittest、pytest等)来调整实现方式。例如,在unittest框架中,可以使用setUp和tearDown方法代替自定义的setup和teardown方法。在pytest框架中,可以使用yield语句和fixture功能来实现类似的效果。
数据驱动测试
使用ddt库提供的装饰器,如@data、@unpack等,来实现数据驱动的测试。
@data和@unpack是ddt(Data-Driven Tests)库提供的装饰器,用于实现数据驱动的测试。以下是一个使用@data和@unpack装饰器的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from ddt import ddt, data, unpack import unittest import requests class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @data( ("Alice", "password123", 200), ("Bob", "invalid_password", 401), ("", "", 400), (None, None, 400), ) @unpack def test_login(self, username, password, expected_status_code): url = f"{self.base_url}/login" payload = {"username": username, "password": password} response = requests.post(url, json=payload) self.assertEqual(response.status_code, expected_status_code) if __name__ == "__main__": unittest.main()
|
在这个例子中:
我们使用@data装饰器定义了一个包含多个测试数据集的元组列表。每个元组代表一个特定的测试场景,包括用户名、密码和预期的HTTP状态码。
使用@unpack装饰器,我们可以将元组中的值直接解包并作为测试函数的参数。这样在测试函数内部,我们就可以直接使用这些变量名,而不需要通过索引来访问它们。
在test_login方法中,我们根据提供的用户名、密码和预期状态码发送登录请求,并验证响应的状态码是否与预期相符。
通过这种方式,我们可以使用@data和@unpack装饰器轻松地为同一个测试函数定义多个不同的测试场景,从而提高测试覆盖率和代码的可读性。
数据类装饰器,@dataclass
把一个类装饰成一个数据类–方便后面取值,后面就可以直接调用此类的属性拿到值了,不需要像字典一样使用点get()或者[‘键名’]的方式去拿数据
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
| from dataclasses import dataclass
@dataclass class Person: name: str age: int email: str
person = Person("Alice", 25, "alice@example.com") print(person.name) print(person.age) print(person.email)
person2_dict = { "name": "Bob", "age": 30, "email": "Bob@example.com" } person2 = Person(**person2_dict)
print(person2.name) print(person2.age) print(person2.email)
|
性能测试
@timer:测量测试函数的执行时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import time def timer(func): def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print(f"{func.__name__} executed in {end_time - start_time:.6f} seconds") return result return wrapper class MyTestCase(unittest.TestCase): @timer def test_my_api(self): time.sleep(2) if __name__ == "__main__": unittest.main()
|
在这个例子中:
我们定义了一个名为timer的装饰器,它接受一个函数作为参数,并返回一个新的函数(即wrapper)。
在wrapper函数内部,我们首先记录开始时间,然后调用原始函数并获取其结果,最后记录结束时间。
我们计算并打印出被装饰的函数的执行时间。
在MyTestCase类中,我们使用@timer装饰器装饰了test_my_api方法。当我们运行这个测试用例时,装饰器会测量并打印出该方法的执行时间。
请注意,这个timer装饰器只是一个简单的示例,实际使用中可能需要根据具体需求进行调整。例如,你可能希望将执行时间记录到日志文件中,或者在测试报告中包含执行时间信息等。
@profile(使用cProfile库):进行CPU性能分析。
以下是一个使用@profile装饰器(配合cProfile库)进行CPU性能分析的简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import cProfile from pstats import Stats import unittest import requests class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @profile def test_my_api(self): url = f"{self.base_url}/resource" response = requests.get(url) self.assertEqual(response.status_code, 200) if __name__ == "__main__": profiler = cProfile.Profile() test_case = MyTestCase() test_case.test_my_api() profiler.create_stats() stats = Stats(profiler) stats.sort_stats('cumulative') stats.print_stats()
|
在这个例子中:
我们使用@profile装饰器装饰了test_my_api方法。当运行这个测试用例时,cProfile库会记录该方法的CPU性能数据。
在测试用例执行完毕后,我们创建了一个Stats对象,并将Profiler的统计数据传递给它。
我们调用sort_stats方法对统计数据进行排序。在这个例子中,我们选择了按累计时间排序。
最后,我们调用print_stats方法打印出性能分析结果。
请注意,这个示例只是一个基本的使用方式,实际使用中可能需要根据具体需求进行调整。例如,你可能希望将性能分析结果保存到文件中,或者使用更复杂的统计和过滤选项等。另外,cProfile库主要用于CPU性能分析,如果你需要进行内存或其他资源的性能分析,可能需要使用其他工具或库。
日志记录
@log_test:记录测试的开始和结束,以及测试结果。
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
| import unittest class TestLogger: def log_start(self, test_name): print(f"Starting test: {test_name}") def log_end(self, test_name, result): print(f"Ending test: {test_name}") if result: print("Test passed.") else: print("Test failed.") def log_test(func): def wrapper(test_logger, *args, **kwargs): test_name = func.__name__ test_logger.log_start(test_name) result = func(test_logger, *args, **kwargs) test_logger.log_end(test_name, result) return result return wrapper class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.test_logger = TestLogger() @log_test def test_my_api(self, test_logger): url = "http://example.com/api" response = requests.get(url) self.assertEqual(response.status_code, 200) if __name__ == "__main__": unittest.main()
|
在这个例子中:
我们定义了一个名为TestLogger的类,它包含两个方法:log_start和log_end,分别用于记录测试的开始和结束,以及测试结果。
我们定义了一个名为log_test的装饰器,它接受一个函数作为参数,并返回一个新的函数(即wrapper)。
在wrapper函数内部,我们首先获取被装饰的函数名,然后调用log_start方法记录测试的开始。
我们调用原始函数并获取其结果。
我们调用log_end方法记录测试的结束,并传递测试结果。
在MyTestCase类中,我们创建了一个TestLogger实例,并在setUpClass方法中设置为类变量。
我们使用@log_test装饰器装饰了test_my_api方法。当我们运行这个测试用例时,装饰器会记录测试的开始和结束,以及测试结果。
请注意,这个log_test装饰器只是一个简单的示例,实际使用中可能需要根据具体需求进行调整。例如,你可能希望将日志信息写入文件,或者使用更复杂的日志格式和级别等。另外,这个示例中的日志输出是简单的文本形式,如果你需要更丰富的日志功能,可以考虑使用Python的logging模块。
异常处理
@catch_exceptions:捕获并处理测试函数中可能抛出的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import unittest def catch_exceptions(func): def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: print(f"Exception caught during test: {e}") return False return wrapper class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @catch_exceptions def test_my_api(self): url = f"{self.base_url}/resource" response = requests.get(url) self.assertEqual(response.status_code, 200) if __name__ == "__main__": unittest.main()
|
在这个例子中:
我们定义了一个名为catch_exceptions的装饰器,它接受一个函数作为参数,并返回一个新的函数(即wrapper)。
在wrapper函数内部,我们使用try-except语句包裹原始函数的调用。如果在执行原始函数时抛出了异常,我们将捕获这个异常,并打印出异常信息。
如果捕获到异常,我们返回False,表示测试失败。否则,我们返回原始函数的返回值。
在MyTestCase类中,我们使用@catch_exceptions装饰器装饰了test_my_api方法。当我们运行这个测试用例时,装饰器会捕获并处理该方法中可能抛出的异常。
请注意,这个catch_exceptions装饰器只是一个简单的示例,实际使用中可能需要根据具体需求进行调整。例如,你可能希望对不同类型的异常进行不同的处理,或者在捕获异常后执行一些清理操作等。另外,虽然这个装饰器可以帮助我们在测试函数中捕获和处理异常,但在编写测试代码时,通常建议明确预期可能会抛出的异常,并使用assertRaises等断言来验证它们。这样可以提供更清晰的测试意图和更好的错误消息。
重试机制
@retry:在测试失败时自动重试指定次数
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
| import unittest def retry(attempts=3, delay=1): def decorator(func): def wrapper(*args, **kwargs): for i in range(attempts): try: return func(*args, **kwargs) except Exception as e: print(f"Test failed on attempt {i + 1}: {e}") if i < attempts - 1: time.sleep(delay) raise Exception(f"Test failed after {attempts} attempts") return wrapper return decorator class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @retry(attempts=3, delay=2) def test_my_api(self): url = f"{self.base_url}/resource" response = requests.get(url) self.assertEqual(response.status_code, 200) if __name__ == "__main__": unittest.main()
|
在这个例子中:
我们定义了一个名为retry的装饰器,它接受两个可选参数:attempts(重试次数,默认为3)和delay(每次重试之间的延迟时间,默认为1秒)。
retry装饰器内部定义了一个新的装饰器函数(即decorator),它接受一个函数作为参数,并返回一个新的函数(即wrapper)。
在wrapper函数内部,我们使用一个循环来尝试调用原始函数。如果在执行原始函数时抛出了异常,我们将捕获这个异常,并打印出异常信息。
如果捕获到异常并且还有剩余的重试次数,我们将暂停一段时间(由delay参数指定),然后再次尝试调用原始函数。
如果所有重试都失败了,我们将重新抛出最后一个捕获到的异常。
在MyTestCase类中,我们使用@retry装饰器装饰了test_my_api方法,并指定了重试次数和延迟时间。当我们运行这个测试用例时,装饰器会自动重试该方法,直到成功或达到最大重试次数。
请注意,这个retry装饰器只是一个简单的示例,实际使用中可能需要根据具体需求进行调整。例如,你可能希望对不同类型的异常进行不同的重试策略,或者在重试之间执行一些清理操作等。另外,虽然重试机制可以在某些情况下帮助处理临时性故障,但过度依赖重试可能会掩盖系统中的根本问题。因此,在使用重试机制时应谨慎考虑其适用性和可能的副作用。
权限和认证
@with_auth:为测试函数添加特定的认证信息或权限
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
| import unittest class AuthManager: def __init__(self, username, password): self.username = username self.password = password def authenticate(self): pass def with_auth(username, password): def decorator(func): def wrapper(*args, **kwargs): auth_manager = AuthManager(username, password) auth_manager.authenticate() return func(auth_manager, *args, **kwargs) return wrapper return decorator class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @with_auth("test_user", "test_password") def test_my_api(self, auth_manager): url = f"{self.base_url}/resource" headers = {"Authorization": auth_manager.access_token} response = requests.get(url, headers=headers) self.assertEqual(response.status_code, 200) if __name__ == "__main__": unittest.main()
|
在这个例子中:
我们定义了一个名为AuthManager的类,它用于管理认证信息和执行认证操作。
我们定义了一个名为with_auth的装饰器,它接受两个参数:username和password。
with_auth装饰器内部定义了一个新的装饰器函数(即decorator),它接受一个函数作为参数,并返回一个新的函数(即wrapper)。
在wrapper函数内部,我们创建了一个AuthManager实例,并使用提供的用户名和密码进行初始化。
我们调用authenticate方法对AuthManager实例进行认证。
我们将认证后的AuthManager实例传递给原始函数,并返回其结果。
在MyTestCase类中,我们使用@with_auth装饰器装饰了test_my_api方法,并提供了用户名和密码。当我们运行这个测试用例时,装饰器会先执行认证操作,然后将认证后的AuthManager实例传递给test_my_api方法。
请注意,这个with_auth装饰器只是一个简单的示例,实际使用中可能需要根据具体需求进行调整。例如,你可能需要处理更复杂的认证流程,或者支持多种不同的认证方式等。另外,这个示例中的AuthManager类和authenticate方法只是为了演示目的而编写的,实际使用时应替换为实际的认证逻辑和数据。
参数化测试
@parametrize(使用pytest库):为测试函数提供多个参数组合
以下是一个使用@pytest.mark.parametrize装饰器(来自pytest库)为测试函数提供多个参数组合的简单示例:
首先,确保已经安装了pytest库:pip install pytest
然后,可以使用以下代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import pytest import requests class MyTestCase: @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @pytest.mark.parametrize("username, password, expected_status_code", [ ("Alice", "password123", 200), ("Bob", "invalid_password", 401), ("", "", 400), (None, None, 400), ]) def test_login(self, username, password, expected_status_code): url = f"{self.base_url}/login" payload = {"username": username, "password": password} response = requests.post(url, json=payload) assert response.status_code == expected_status_code if __name__ == "__main__": pytest.main()
|
在这个例子中:
我们使用@pytest.mark.parametrize装饰器装饰了test_login方法。该装饰器接受两个参数:一个参数名列表和一个参数值列表。
参数名列表包含了被装饰的测试函数期望接收的参数名称。
参数值列表是一个包含多个元组的列表,每个元组代表一组特定的参数值组合。
当我们运行这个测试用例时,pytest会为test_login方法生成多个实例,每个实例使用一组不同的参数值。这样,我们可以用一种简洁的方式为同一个测试函数提供多个不同的输入和预期输出。
请注意,虽然这个例子中我们没有使用unittest.TestCase类,但你可以将这个测试用例与unittest.TestCase结合使用,只需要在测试类中继承unittest.TestCase,并使用unittest的断言方法即可。另外,pytest提供了许多其他功能和装饰器,可以根据需要进行探索和使用。
条件执行
@skip_if:在满足特定条件时跳过测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import unittest def skip_if(condition): def decorator(func): def wrapper(*args, **kwargs): if condition: print(f"Test skipped because condition is satisfied: {condition}") return return func(*args, **kwargs) return wrapper return decorator class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @skip_if(os.environ.get("SKIP_FLAKY_TESTS", False)) def test_flaky_api(self): url = f"{self.base_url}/flaky-resource" response = requests.get(url) self.assertEqual(response.status_code, 200) if __name__ == "__main__": unittest.main()
|
在这个例子中:
我们定义了一个名为skip_if的装饰器,它接受一个参数:一个表示条件的表达式。
skip_if装饰器内部定义了一个新的装饰器函数(即decorator),它接受一个函数作为参数,并返回一个新的函数(即wrapper)。
在wrapper函数内部,我们检查提供的条件是否为真。如果条件为真,我们将打印一条消息并直接返回,从而跳过测试。否则,我们将调用原始函数并返回其结果。
在MyTestCase类中,我们使用@skip_if装饰器装饰了test_flaky_api方法,并提供了一个条件表达式。这个条件表达式检查环境变量”SKIP_FLAKY_TESTS”是否存在且值为True。如果我们设置了这个环境变量,那么这个测试将被跳过。
请注意,这个skip_if装饰器只是一个简单的示例,实际使用中可能需要根据具体需求进行调整。例如,你可能希望对不同类型的条件使用不同的装饰器,或者在跳过测试时执行一些清理操作等。另外,虽然跳过测试可以在某些情况下避免不必要的失败和复杂性,但过度依赖跳过机制可能会掩盖系统中的问题。因此,在使用跳过机制时应谨慎考虑其适用性和可能的副作用。
@run_only_if:在满足特定条件时执行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import unittest def run_only_if(condition): def decorator(func): def wrapper(*args, **kwargs): if not condition: print(f"Test skipped because condition is not satisfied: {condition}") return return func(*args, **kwargs) return wrapper return decorator class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @run_only_if(os.environ.get("RUN_SPECIFIC_TESTS", False)) def test_specific_api(self): url = f"{self.base_url}/specific-resource" response = requests.get(url) self.assertEqual(response.status_code, 200) if __name__ == "__main__": unittest.main()
|
在这个例子中:
我们定义了一个名为run_only_if的装饰器,它接受一个参数:一个表示条件的表达式。
run_only_if装饰器内部定义了一个新的装饰器函数(即decorator),它接受一个函数作为参数,并返回一个新的函数(即wrapper)。
在wrapper函数内部,我们检查提供的条件是否为真。如果条件为假,我们将打印一条消息并直接返回,从而跳过测试。否则,我们将调用原始函数并返回其结果。
在MyTestCase类中,我们使用@run_only_if装饰器装饰了test_specific_api方法,并提供了一个条件表达式。这个条件表达式检查环境变量”RUN_SPECIFIC_TESTS”是否存在且值为True。如果我们设置了这个环境变量,那么这个测试将被执行。
请注意,这个run_only_if装饰器只是一个简单的示例,实际使用中可能需要根据具体需求进行调整。例如,你可能希望对不同类型的条件使用不同的装饰器,或者在执行测试前执行一些初始化操作等。另外,虽然选择性地执行测试可以在某些情况下提高测试效率和针对性,但过度依赖这种机制可能会导致部分测试被忽视或遗漏。因此,在使用run_only_if装饰器时应谨慎考虑其适用性和可能的副作用。
报告和统计
@report_result:自定义测试结果的报告格式或统计数据
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
| import unittest class CustomTestResult(unittest.TestResult): def __init__(self): super().__init__() self.success_count = 0 self.failure_count = 0 self.error_count = 0 def addSuccess(self, test): super().addSuccess(test) self.success_count += 1 def addFailure(self, test, err): super().addFailure(test, err) self.failure_count += 1 def addError(self, test, err): super().addError(test, err) self.error_count += 1 def report_result(func): def wrapper(*args, **kwargs): result = CustomTestResult() test_method = getattr(args[0], func.__name__) test_method(result) print(f"Test results: {result.success_count} successes, {result.failure_count} failures, {result.error_count} errors") return result return wrapper class MyTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = "http://example.com/api" @report_result def test_my_api(self, result): url = f"{self.base_url}/resource" response = requests.get(url) self.assertEqual(response.status_code, 200) if __name__ == "__main__": suite = unittest.TestLoader().loadTestsFromTestCase(MyTestCase) runner = unittest.TextTestRunner() runner.run(suite)
|
在这个例子中:
我们定义了一个名为CustomTestResult的类,它继承自unittest.TestResult并添加了自定义的计数器属性。
我们重写了addSuccess、addFailure和addError方法,以便在测试成功、失败或出现错误时更新自定义计数器。
我们定义了一个名为report_result的装饰器,它接受一个函数作为参数,并返回一个新的函数(即wrapper)。
在wrapper函数内部,我们创建了一个CustomTestResult实例,并获取被装饰的测试方法。
我们调用测试方法并将CustomTestResult实例传递给它,以便收集测试结果。
我们打印出自定义的测试结果报告,包括成功、失败和错误的数量。
在MyTestCase类中,我们使用@report_result装饰器装饰了test_my_api方法。当我们运行这个测试用例时,装饰器会收集测试结果并打印出自定义的报告。
请注意,这个report_result装饰器只是一个简单的示例,实际使用中可能需要根据具体需求进行调整。例如,你可能希望生成更复杂的报告格式,或者将报告保存到文件或数据库等。另外,虽然这个例子中我们没有直接使用unittest.TextTestRunner来运行测试,但在实际项目中,你可以将自定义的CustomTestResult类与unittest.TextTestRunner结合使用,以实现更灵活的测试报告功能。