Mocking In C

Why Mocking is Useful:

  • Isolation of Code Under Test: It helps test specific functions or modules independently of their dependencies.
  • Controlled Environment: It allows us to simulate edge cases, such as error conditions or specific responses from dependencies.
  • Improves Reliability: Tests become consistent because they don't rely on external factors like network availability or database states.
  • Simplifies Debugging: When a test fails, we know the problem lies within the code under test, not its dependencies.
  • Approach 1: Basic Approach

    Example
    
    #include <stdio.h>
    
    #include <stdbool.h>
    
    // Original function (to be mocked during testing)
    
    bool send_to_payment_gateway(double amount) {
    
        // Simulate sending to a payment gateway
    
        printf("Real: Sending $%.2f to payment gateway...\n", amount);
    
        return true; // Assume payment is always successful
    
    }
    
    // Function pointer for mocking
    
    bool (*send_payment_ptr)(double) = send_to_payment_gateway;
    
    // Function to process payment
    
    bool process_payment(double amount) {
    
        if (amount <= 0) {
    
            printf("Error: Invalid payment amount.\n");
    
            return false;
    
        }
    
        return send_payment_ptr(amount); // Use function pointer for payment
    
    }
    
    // Mock function
    
    bool mock_send_to_payment_gateway(double amount) {
    
        printf("Mock: Pretending to send $%.2f to payment gateway...\n", amount);
    
        return (amount <= 100.0); // Mock behavior: succeed if <= $100
    
    }
    
    int main() {
    
        // Test with the real implementation
    
        printf("=== Using Real Payment Gateway ===\n");
    
        bool result = process_payment(150.0);
    
        printf("Payment Status: %s\n\n", result ? "Success" : "Failure");
    
        // Replace with mock implementation for testing
    
        send_payment_ptr = mock_send_to_payment_gateway;
    
        printf("=== Using Mock Payment Gateway ===\n");
    
        result = process_payment(150.0); // Should fail because > $100
    
        printf("Payment Status: %s\n", result ? "Success" : "Failure");
    
        result = process_payment(50.0); // Should succeed because <= $100
    
        printf("Payment Status: %s\n", result ? "Success" : "Failure");
    
        return 0;
    
    }
    

Output:

Output

-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

Explanation:

  1. Identifying the Dependency
  • The first step in mocking is identifying the external dependency we want to replace with a mock. In our case, the sendtopayment_gateway function is the external dependency that interacts with an external payment gateway. This function is responsible for sending payment data and receiving a response indicating whether the payment was successful.
  • However, calling the real payment gateway might be impractical or expensive for unit testing. Instead, we will create a mock version of this function to simulate its behavior during tests.
  1. Creating the Real Function

In practical situations, the sendtopayment_gateway function typically interfaces with an external service, like a financial institution's payment platform or a third-party API. The primary purpose of this function is to transmit payment information and receive a status indicating either success or failure, depending on the feedback received from the external system.

The actual implementation might look something like this:

  • It sends a request to the payment gateway.
  • It waits for a response (usually a success or failure message).
  • It returns the response to the caller (e.g., success or failure).

For unit testing purposes, we substitute this function with a mock implementation to replicate its behavior.

  1. Leveraging Function Pointers for Mocking

To replace the real sendtopayment_gateway function with a mock, we use function pointers in C. A function pointer is a variable that stores the address of a function. By pointing the function pointer to the objective function initially and then switching it to the mock function during tests, we can control which version of the function gets called.

  • Initially, the function pointer points to the real sendtopayment_gateway.
  • During testing, we change the function pointer to point to the mock version, mocksendtopaymentgateway.
  1. Implementing the Mock Function

The dummy function is a basic representation of the authentic function. It does not execute the real task of interacting with an external system. Instead, it mimics the functionality of the actual function in a regulated environment.

  • In a scenario where the payment amount is $100 or lower, the mock function has the ability to indicate a successful payment processing outcome.
  • However, if the payment amount exceeds $100, the mock function can mimic a transaction failure to illustrate how the payment gateway handles rejections of high-value transactions.

It enables us to evaluate the functionality of the payment processing system without depending on the live payment gateway.

  1. Testing Using the Actual Functionality

In a common situation (beyond testing purposes), the sendpaymentptr function pointer will be directed to the authentic sendtopaymentgateway. This implies that upon calling processpayment, it will trigger the genuine payment function.

During the testing phase, we replicate various scenarios by substituting the function pointer with the mock function. For instance, we might want to assess the behavior of the payment processing system in cases of successful or failed payments. Through manipulating the mock function's actions, we can evaluate these situations without initiating genuine payment transactions.

  1. Switching to the Mock Function

Once the dummy function is established, we have the ability to change the function pointer to the dummy version when conducting tests. This enables us to mimic the functionality of the sendtopayment_gateway function under various scenarios, including:

  • Successful Payment: The dummy function is capable of providing a successful status when the payment amount is below a defined limit.
  • Unsuccessful Payment: In cases where the payment amount surpasses the set limit, the dummy function can replicate a failure by issuing a failed status.

This method enables us to manage the testing environment and concentrate on validating the payment processing algorithm without relying on actual external factors.

  1. Behavior Testing

By using the mock function, we can test various cases, such as:

  • Valid Payments: Ensure that the process_payment function handles payments correctly when the mock function returns a success status.
  • Edge Cases: Simulate different failure conditions (e.g., invalid amounts) by modifying the behavior of the mock function.
  • Error Handling: Test how the system reacts to failure responses from the mock function.

The primary advantage of using mocking is its ability to segregate the code being tested, allowing us to concentrate on validating the functionality of the process_payment function across various scenarios without being concerned about the specific details of the payment gateway implementation.

Complexity analysis:

Time Complexity:

Actual Execution (sendtopayment_gateway):

  • The implemented function carries out a basic task: displaying a message and indicating success.
  • Time Complexity: O(1) (constant time), since it goes through a set sequence of actions irrespective of the input magnitude.

Mock Implementation (mocksendtopaymentgateway):

The simulated function also carries out a basic task: displaying a message and validating a condition (amount <= 100.0).

The computational complexity is O(1) (constant time), since the comparison is not influenced by the size of the input.

The processpayment function validates the amount (ensuring it is not less than or equal to zero) and invokes the function pointer sendpayment_ptr. The time complexity of this function is O(1) because it involves constant-time checks and a single function call.

Overall Time Complexity: O(1) remains constant for both actual and simulated implementations.

Space Complexity:

Real Implementation:

  • The function does not allocate any dynamic memory or maintain any large data structures.
  • It only prints a message and returns a boolean value.
  • Space Complexity: O(1) (constant space).

Mock Implementation:

  • In the same manner, the mock function conserves memory by utilizing a fixed amount of space to hold the boolean outcome of the comparison (amount <= 100.0).
  • Space Complexity: O(1) (constant space).

The process_payment function operates without utilizing any extra memory apart from the input parameters (amount) and the output acquired from the function pointer. This results in a Space Complexity of O(1).

Overall Space Complexity: O(1).

Approach 2: Mocking Using Preprocessor Macros

In this scenario, we will be evaluating a function named fetchdata which relies on an external function called getfromserver. To carry out the testing process effectively, we will implement a mock version of getfrom_server.

Program:

Example

#include <stdio.h>

#include <string.h>

// Real function (to be mocked)

const char* get_from_server(const char* request) {

    printf("Real: Fetching data from server for request: %s\n", request);

    return "Real Server Response";

}

// Function to fetch data (uses the real or mock implementation)

const char* fetch_data(const char* request) {

    return get_from_server(request); // This call will be replaced in testing

}

// Mock function

const char* mock_get_from_server(const char* request) {

    printf("Mock: Simulating server response for request: %s\n", request);

    if (strcmp(request, "test") == 0) {

        return "Mock Server Response";

    }

    return "Mock Default Response";

}

// During testing, replace get_from_server with mock_get_from_server

#define get_from_server mock_get_from_server

int main() {

    printf("=== Testing with Mock Implementation ===\n");

    const char* response = fetch_data("test");

    printf("Response: %s\n", response);

    response = fetch_data("unknown");

    printf("Response: %s\n", response);

    return 0;

}

Output:

Output

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

Explanation:

Step 1: Identify the Dependency

  • Find the function or component in our program that depends on an external resource or system, such as a server, database, or hardware device.
  • For example, a function that fetches data from a server might be difficult to test because it depends on server availability.
  • Step 2: Create the Real Function

  • The objective function performs the actual operation, such as fetching data from the server or processing a request.
  • This function will remain unchanged and will be used in production.
  • Step 3: Design the Mock Function

  • Write a mock version of the real function that simulates its behavior in a controlled and predictable manner.
  • The mock function should provide consistent outputs or simulate edge cases needed for testing.
  • For instance, the mock function could return predefined responses or simulate errors.
  • Step 4: Replace the Real Function with a Macro

  • Use the #define preprocessor directive to replace calls to the real function with calls to the mock function.
  • This replacement is done in the test file or test configuration, ensuring the main code remains untouched.
  • The macro ensures that every call to the real function in our code is replaced with the mock function during testing.
  • Step 5: Test with the Mock Function

  • Run wer tests, focusing on the behavior of the function under test while the mock function replaces the real one.
  • The mock function provides controlled inputs or behaviors, enabling us to test edge cases or error conditions easily.
  • Step 6: Switch Back to the Real Function

  • For production, simply remove or comment out the macro definition in our test file.
  • The preprocessor will now compile the code with the real function instead of the mock.
  • Complexity analysis:

Time Complexity:

Mock Function:

The simulated payment gateway provides predetermined outcomes depending on specific criteria, such as the validity of the payment amount. This process is instantaneous, requiring only a validation check before delivering a pre-set response.

The efficiency of the mock function is denoted by a time complexity of O(1) (constant time). Its performance remains invariant regardless of input size, swiftly producing an output following a straightforward comparison.

The function responsible for payment processing validates the input data to confirm its accuracy, such as verifying that the payment amount is a positive value, before initiating communication with the simulated payment gateway. As these tasks involve straightforward conditional validations, they are executed in constant time.

The time complexity of this payment processing function is O(1) or constant time. This is because the function does not include any intricate processes like loops or recursive functions, ensuring that its execution time remains constant irrespective of the input provided.

Overall Time Complexity:

When considering the performance of both the mock function and the payment processing function, it is evident that they both execute tasks in constant time. As a result, the overall time complexity for a single test case is sustained at O(1). This characteristic contributes to the efficiency of the tests, as they are not influenced by external variables such as network latency or server response time.

Space Complexity:

Mock Function:

  • The mock function employs a static memory allocation to hold its operations, such as validating the payment amount. It operates without the need for dynamic memory allocation, providing straightforward feedback.
  • Space Complexity: O(1) (constant space). The mock function operates without employing extra data structures or dynamic memory, ensuring minimal space requirements.

Payment Processing Operation:

  • The payment processing operation also maintains a constant space usage by storing minimal variables, such as the payment amount, and interacting with the simulated payment gateway. It avoids the need to create extensive data structures or allocate significant resources.
  • Space Complexity: O(1) (constant space). The operation operates efficiently without necessitating extra memory apart from its initial input and a limited number of variables.

Overall Space Efficiency:

The reason the overall space complexity remains O(1) is that both the simulated function and the payment processing function utilize a consistent amount of memory. This means that there is no requirement for extensive buffers, complex data structures, or external resources, ensuring optimal space utilization.

Properties:

  1. Cost-Effective
  • Real payment gateways often involve transaction fees, and using them for testing could incur unnecessary costs. Mocking eliminates this issue, as no real payments are processed.
  • Example: We can test various payment scenarios without spending money on actual transactions, making testing more economical.
  1. Error Handling and Edge Case Testing
  • Mocking allows we to test error handling by simulating a variety of edge cases. We can easily test scenarios that might be difficult or impossible to replicate using a real payment gateway, such as network failures or specific error codes.
  • Example: We can simulate an expired credit card or a payment gateway rejection to test how the system responds to these edge cases.
  1. Reliabilitys
  • External systems (like payment gateways) may be unreliable during testing due to factors like downtime, server issues, or rate limits. Mocks are stable and predictable, ensuring that our tests are not affected by such external factors.
  • Example: During testing, the mock payment gateway will always return the same result for a given input, ensuring that tests don't fail due to external system issues.
  1. Flexibility
  • Mocks can easily be adjusted to simulate different conditions. This flexibility is essential when we need to test a wide range of scenarios, such as handling different types of payment methods or transaction amounts.
  • Example: We can modify the mock function to return different results based on input, simulating various payment outcomes like approvals, declines, or errors.

Input Required

This code uses input(). Please provide values below: