C 语言 Mocking

2025 年 5 月 13 日 | 阅读 10 分钟

引言

C语言中的Mocking指的是创建函数或组件的模拟版本,以在受控环境中模拟它们的行为。这项技术广泛用于软件测试,尤其是单元测试,旨在隔离被测试的代码,并用可预测和可控的替代品替换依赖项。Mocking使我们能够验证代码如何与其他组件交互,而无需依赖实际的实现。

为什么Mocking很有用

  • 隔离被测试代码:它有助于独立于依赖项测试特定的函数或模块。
  • 受控环境:它允许我们模拟边缘情况,例如错误条件或来自依赖项的特定响应。
  • 提高可靠性:测试变得一致,因为它们不依赖于网络可用性或数据库状态等外部因素。
  • 简化调试:当测试失败时,我们知道问题出在被测试代码中,而不是其依赖项中。

方法 1:基本方法

输出

-Using Real Payment Gateway- 
Real: Sending $150.00 to payment gateway...
Payment Status: Success
-Using Mock Payment Gateway- 
Mock: Pretending to send $150.00 to payment gateway...
Payment Status: Failure
Mock: Pretending to send $50.00 to payment gateway...
Payment Status: Success

说明

1. 识别依赖项

  • Mocking的第一步是识别我们想要用模拟对象替换的外部依赖项。在我们的例子中,send_to_payment_gateway函数是与外部支付网关交互的外部依赖项。此函数负责发送支付数据并接收指示支付是否成功的响应。
  • 然而,对于单元测试来说,调用真实的支付网关可能不切实际或成本高昂。相反,我们将创建此函数的一个模拟版本,以在测试期间模拟其行为。

2. 创建真实函数

在实际场景中,send_to_payment_gateway函数将与外部服务(例如银行的支付系统或第三方API)进行通信。此函数将发送支付详细信息,并根据外部系统的响应返回成功或失败状态。

实际实现可能如下所示:

  • 它向支付网关发送请求。
  • 它等待响应(通常是成功或失败消息)。
  • 它将响应返回给调用者(例如,成功或失败)。

对于单元测试,我们用模拟实现替换此函数以模拟其行为。

3. 使用函数指针进行Mocking

为了用模拟函数替换真实的send_to_payment_gateway函数,我们在C语言中使用函数指针。函数指针是存储函数地址的变量。通过最初将函数指针指向目标函数,然后在测试期间将其切换到模拟函数,我们可以控制调用哪个版本的函数。

  • 最初,函数指针指向真实的send_to_payment_gateway。
  • 在测试期间,我们将函数指针更改为指向模拟版本mock_send_to_payment_gateway。

4. 实现模拟函数

模拟函数是真实函数的简化版本。它不执行与外部系统通信的实际工作。相反,它在受控条件下模拟真实函数的行为。

例如

  • 如果金额小于或等于$100,模拟函数可以返回成功(就像支付成功一样)。
  • 如果金额大于$100,模拟函数可能会模拟失败(表示支付网关拒绝大额交易的场景)。

它允许我们测试支付处理逻辑的行为,而无需依赖实际的支付网关。

5. 使用真实函数进行测试

在典型场景(测试之外)中,send_payment_ptr函数指针将指向真实的send_to_payment_gateway。这意味着当我们调用process_payment时,它将调用实际的支付函数。

在测试期间,我们通过将函数指针切换到模拟函数来模拟不同的条件。例如,我们可能想测试当支付成功或失败时支付处理系统的行为。通过控制模拟函数的行为,我们可以测试这些场景,而无需实际发出真实支付请求。

6. 切换到模拟函数

设置好模拟函数后,我们可以在测试期间将函数指针切换到模拟版本。这使我们能够根据不同条件模拟send_to_payment_gateway函数的行为,例如:

  • 有效支付:如果支付金额低于特定阈值,模拟函数可以返回成功状态。
  • 无效支付:如果支付金额超过阈值,模拟函数可以通过返回失败状态来模拟失败。

这种方法允许我们控制测试环境,并专注于测试支付处理逻辑,而无需真实世界的依赖项。

7. 测试行为

通过使用模拟函数,我们可以测试各种情况,例如:

  • 有效支付:确保当模拟函数返回成功状态时,process_payment函数正确处理支付。
  • 边缘情况:通过修改模拟函数的行为来模拟不同的失败条件(例如,无效金额)。
  • 错误处理:测试系统如何响应模拟函数的失败响应。

Mocking的关键好处是它隔离了被测试代码,因此我们可以专注于验证process_payment函数在不同场景下是否行为正确,而无需担心实际支付网关的实现。

复杂度分析

时间复杂度

实际实现(send_to_payment_gateway)

  • 真实函数执行一个简单的操作:打印消息并返回true。
  • 时间复杂度: O(1)(常数时间),因为它执行固定数量的步骤,无论输入大小如何。

模拟实现(mock_send_to_payment_gateway)

  • 模拟函数也执行一个简单的操作:打印消息并检查条件(amount <= 100.0)。
  • 时间复杂度: O(1)(常数时间),因为比较与输入大小无关。

process_payment函数

  • 该函数检查金额是否有效(amount <= 0),然后调用函数指针(send_payment_ptr)。
  • 时间复杂度: O(1),因为操作是常数时间检查和单个函数调用。

整体时间复杂度: O(1),适用于真实和模拟实现。

空间复杂度

真实实现

  • 该函数不分配任何动态内存或维护任何大型数据结构。
  • 它只打印消息并返回布尔值。
  • 空间复杂度: O(1)(常数空间)。

模拟实现

  • 同样,模拟函数使用常数空间来存储条件(amount <= 100.0)的布尔结果。
  • 空间复杂度: O(1)(常数空间)。

process_payment函数

  • 除了参数(amount)和从函数指针返回的结果之外,它不使用任何额外内存。
  • 空间复杂度: O(1)。

整体空间复杂度:O(1)。

方法二:使用预处理器宏进行Mocking

在此方法中,我们将测试一个依赖于外部get_from_server函数的fetch_data函数。在测试期间,我们将使用get_from_server的模拟实现。

程序

输出

Testing with Mock Implementation
Real: Fetching data from server for request: test
Response: Real Server Response
Real: Fetching data from server for request: unknown
Response: Real Server Response

说明

步骤1:识别依赖项

  • 找到程序中依赖于外部资源或系统(例如服务器、数据库或硬件设备)的函数或组件。
  • 例如,一个从服务器获取数据的函数可能难以测试,因为它依赖于服务器的可用性。

步骤2:创建真实函数

  • 目标函数执行实际操作,例如从服务器获取数据或处理请求。
  • 此函数将保持不变,并将在生产中使用。

步骤3:设计模拟函数

  • 编写真实函数的模拟版本,以受控和可预测的方式模拟其行为。
  • 模拟函数应提供一致的输出或模拟测试所需的边缘情况。
  • 例如,模拟函数可以返回预定义响应或模拟错误。

步骤4:用宏替换真实函数

  • 使用#define预处理器指令将对真实函数的调用替换为对模拟函数的调用。
  • 此替换在测试文件或测试配置中完成,确保主代码保持不变。
  • 该宏确保在测试期间,代码中对真实函数的每次调用都替换为模拟函数。

步骤5:使用模拟函数进行测试

  • 运行我们的测试,重点关注被测试函数的行为,同时模拟函数替换了真实函数。
  • 模拟函数提供受控输入或行为,使我们能够轻松测试边缘情况或错误条件。

步骤6:切换回真实函数

  • 对于生产,只需删除或注释掉测试文件中的宏定义。
  • 预处理器现在将使用真实函数而不是模拟函数编译代码。

复杂度分析

时间复杂度

模拟函数

  • 模拟支付网关只是根据某些条件(例如,支付金额是否有效)返回预定义的响应。这是一个常数时间操作,因为它只涉及检查输入并返回固定响应。
  • 时间复杂度:O(1)(常数时间)。模拟函数的执行时间不依赖于输入的大小,因为它在简单比较后直接返回结果。

支付处理函数

  • 支付处理函数检查输入的有效性(例如,确保支付金额为正),然后调用模拟支付网关。由于所有这些操作都是简单的条件检查,因此它们也需要常数时间。
  • 时间复杂度:O(1)(常数时间)。该函数不涉及任何复杂的循环或递归调用等操作,因此无论输入如何,其运行时间都是常数。

总体时间复杂度

  • 由于模拟函数和支付处理函数都以常数时间执行操作,因此单个测试用例的整体时间复杂度保持为O(1)。
  • 这使得测试速度很快,因为它们不依赖于网络延迟或服务器响应时间等外部因素。

空间复杂度

模拟函数

  • 模拟函数使用固定量的内存来存储其逻辑(例如,检查支付金额是否有效)。它不需要任何动态内存分配,因为它只返回一个简单的响应。
  • 空间复杂度: O(1)(常数空间)。模拟函数不使用任何额外的数据结构或动态内存。

支付处理函数

  • 支付处理函数也使用常数空间,因为它只存储几个变量(例如,支付金额)并调用模拟支付网关。它不分配大型数据结构或资源。
  • 空间复杂度: O(1)(常数空间)。该函数不需要额外的内存,除了其输入和几个变量。

总体空间复杂度

  • 由于模拟函数和支付处理函数都需要常数内存量,因此整体空间复杂度保持为O(1)。
  • 不需要大缓冲区、数据结构或外部资源,从而提高空间使用效率。

性质

1. 成本效益

  • 真实的支付网关通常涉及交易费用,将其用于测试可能会产生不必要的成本。Mocking消除了这个问题,因为没有实际的支付被处理。
  • 示例:我们可以测试各种支付场景,而无需花费实际交易的费用,使测试更经济。

2. 错误处理和边缘情况测试

  • Mocking允许我们通过模拟各种边缘情况来测试错误处理。我们可以轻松测试使用真实支付网关难以或不可能重现的场景,例如网络故障或特定的错误代码。
  • 示例:我们可以模拟过期信用卡或支付网关拒绝,以测试系统如何响应这些边缘情况。

3. 可靠性

  • 外部系统(如支付网关)在测试期间可能由于停机、服务器问题或速率限制等因素而不可靠。模拟对象稳定且可预测,确保我们的测试不受此类外部因素的影响。
  • 示例:在测试期间,模拟支付网关将始终对给定输入返回相同的结果,确保测试不会因外部系统问题而失败。

4. 灵活性

  • 模拟对象可以很容易地调整以模拟不同的条件。这种灵活性在我们需要测试各种场景时至关重要,例如处理不同类型的支付方式或交易金额。
  • 示例:我们可以修改模拟函数以根据输入返回不同的结果,模拟各种支付结果,如批准、拒绝或错误。