# Testing RPC Handlers

### Comprehensive Testing Strategies for Custom Handlers

[← Back to Building and Integrating Custom RPC Handlers](/core-dev-bootcamp/module07.md)

***

## Introduction

Testing is **critical** for ensuring your RPC handlers work correctly across all scenarios. A well-tested handler catches edge cases, prevents security vulnerabilities, and provides confidence during deployment.

In this section, you'll learn how to structure unit tests using Google Test, create mock objects and fixtures, implement integration tests, and develop comprehensive test cases that cover happy paths, error conditions, and different user roles.

***

## Google Test Framework Overview

Rippled uses **Google Test (gtest)** for unit testing. All tests follow a consistent pattern.

### Test File Structure

**Location**: `src/tests/rpc/handlers/`

```cpp
//------------------------------------------------------------------------------
/*
    Test file for MyHandler RPC command
*/
//==============================================================================

#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/handlers/Handlers.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <test/jtx.h>
#include <gtest/gtest.h>

namespace ripple {
namespace test {

// Test class
class MyHandlerTest : public ::testing::Test {
protected:
    // Setup called before each test
    void SetUp() override {
        // Initialize test fixtures
    }

    // Teardown called after each test
    void TearDown() override {
        // Clean up test fixtures
    }

    // Test helper methods
    Json::Value callHandler(Json::Value const& params);
};

// Test cases follow below

} // namespace test
} // namespace ripple
```

***

## Unit Test Structure

### Basic Test Case

```cpp
// Test that handler validates required parameters
TEST_F(MyHandlerTest, RequiredParametersValidation) {
    // Arrange
    Json::Value params;
    // Don't set required fields

    // Act
    Json::Value result = callHandler(params);

    // Assert
    EXPECT_TRUE(result.isMember(jss::error));
    EXPECT_EQ(result[jss::error_code].asInt(), rpcINVALID_PARAMS);
}

// Test successful operation
TEST_F(MyHandlerTest, SuccessfulOperation) {
    // Arrange
    Json::Value params;
    params[jss::account] = "rN7n7otQDd6FczFgLdlqtyMVrn3NnrcVXs";

    // Act
    Json::Value result = callHandler(params);

    // Assert
    EXPECT_TRUE(result.isMember(jss::status));
    EXPECT_EQ(result[jss::status].asString(), jss::success);
    EXPECT_FALSE(result.isMember(jss::error));
}
```

### Test Naming Convention

Follow this pattern for test names:

```cpp
// Pattern: TEST_F(ClassName, DescriptiveTestName)

// Good names (descriptive)
TEST_F(GetBalanceTest, ReturnsAccountBalance) { }
TEST_F(GetBalanceTest, ReturnsErrorWhenAccountNotFound) { }
TEST_F(GetBalanceTest, ValidatesAccountAddressFormat) { }
TEST_F(GetBalanceTest, WorksWithDifferentLedgerIndices) { }

// Bad names (vague)
TEST_F(GetBalanceTest, Test1) { }
TEST_F(GetBalanceTest, Works) { }
```

***

## Test Fixtures and Setup

### Creating a Comprehensive Test Fixture

```cpp
class AccountInfoTest : public ::testing::Test {
protected:
    std::shared_ptr<Application> app_;
    std::shared_ptr<NetworkOPs> netOps_;
    Account account_;
    Account otherAccount_;

    void SetUp() override {
        // Create test application
        auto config = std::make_unique<Config>();
        config->setupTestDefaults();

        app_ = make_Application(std::move(config));
        netOps_ = &app_->getOPs();

        // Create test accounts
        account_ = Account(generateKeyPair(KeyType::secp256k1).first);
        otherAccount_ = Account(generateKeyPair(KeyType::secp256k1).first);

        // Fund accounts in genesis ledger
        auto const genesisLedger = app_->getLedgerMaster().getValidatedLedger();
        // ... fund accounts
    }

    void TearDown() override {
        app_->stop();
    }

    // Helper to call handler with context
    Json::Value callAccountInfo(std::string const& accountStr) {
        Json::Value params;
        params[jss::account] = accountStr;

        RPC::JsonContext context{
            params,
            *app_,
            *consumer_,
            Role::USER,
            app_->getLedgerMaster().getCurrentLedger(),
            *netOps_,
            app_->getLedgerMaster(),
            1  // API version
        };

        return doAccountInfo(context);
    }
};
```

### Test Utilities with JTX Framework

Rippled provides the **JTX (Joyeux Test eXperience)** framework:

```cpp
#include <test/jtx.h>

class MyHandlerTest : public ::testing::Test {
protected:
    Env env_{*this};  // JTX environment

    void SetUp() override {
        // JTX handles setup automatically
    }
};

// Use JTX to create accounts and transactions
TEST_F(MyHandlerTest, WithJTXFramework) {
    // Create accounts
    auto alice = env_.fund(drops(1000000));
    auto bob = env_.fund(drops(1000000));

    // Submit transactions
    env_(pay(alice, bob, drops(100000)));
    env_.close();  // Close ledger

    // Now query with handler...
}
```

***

## Mocking and Dependency Injection

### Mock Objects for External Dependencies

```cpp
// Mock for LedgerMaster
class MockLedgerMaster {
public:
    MOCK_METHOD(std::shared_ptr<ReadView const>, getCurrentLedger, ());
    MOCK_METHOD(std::shared_ptr<ReadView const>, getValidatedLedger, ());
    MOCK_METHOD(std::shared_ptr<ReadView const>, getLedgerBySeq,
                (LedgerIndex), (const));
    MOCK_METHOD(bool, haveLedger, (), (const));
};

// Mock for Application
class MockApplication {
public:
    MOCK_METHOD(NetworkOPs&, getOPs, ());
    MOCK_METHOD(LedgerMaster&, getLedgerMaster, ());
    MOCK_METHOD(Config&, config, ());
};
```

### Injecting Mocks into Context

```cpp
TEST_F(MyHandlerTest, HandlesLedgerNotAvailable) {
    // Arrange
    MockLedgerMaster mockLedger;
    EXPECT_CALL(mockLedger, getCurrentLedger)
        .WillOnce(::testing::Return(nullptr));

    RPC::JsonContext context{
        params,
        mockApp,
        consumer,
        Role::USER,
        nullptr,  // No ledger
        netOps,
        mockLedger,
        1
    };

    // Act
    Json::Value result = doMyHandler(context);

    // Assert
    EXPECT_EQ(result[jss::error_code].asInt(), rpcNO_CURRENT);
}
```

***

## Happy Path Tests

Tests for successful operations:

```cpp
class GetAccountBalanceTest : public ::testing::Test {
protected:
    Env env_{*this};
    Account alice_;

    void SetUp() override {
        alice_ = env_.fund(drops(1000000000));
        env_.close();
    }
};

// Test 1: Balance query succeeds
TEST_F(GetAccountBalanceTest, ReturnsBalance) {
    Json::Value params;
    params[jss::account] = alice_.human();

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
    EXPECT_TRUE(result.isMember("balance"));
    EXPECT_EQ(result["balance"].asString(), "1000000000");
}

// Test 2: Includes ledger info
TEST_F(GetAccountBalanceTest, IncludesLedgerInfo) {
    Json::Value params;
    params[jss::account] = alice_.human();

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_TRUE(result.isMember(jss::ledger_index));
    EXPECT_TRUE(result.isMember(jss::validated));
    EXPECT_TRUE(result[jss::validated].asBool());
}

// Test 3: Works with different ledger indices
TEST_F(GetAccountBalanceTest, WorksWithDifferentLedgerIndices) {
    auto const ledgerIndex = env_.seq();

    Json::Value params;
    params[jss::account] = alice_.human();
    params[jss::ledger_index] = Json::UInt(ledgerIndex);

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_EQ(result[jss::ledger_index].asUInt(), ledgerIndex);
    EXPECT_FALSE(result[jss::validated].asBool());  // Not validated
}
```

***

## Error Condition Tests

Tests for failure scenarios:

```cpp
class GetAccountBalanceTest : public ::testing::Test {
    // ... setup ...
};

// Test 1: Missing required parameter
TEST_F(GetAccountBalanceTest, ErrorMissingAccount) {
    Json::Value params;
    // Don't set account

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_TRUE(result.isMember(jss::error));
    EXPECT_EQ(result[jss::error].asString(), "invalid_params");
}

// Test 2: Malformed account address
TEST_F(GetAccountBalanceTest, ErrorMalformedAccount) {
    Json::Value params;
    params[jss::account] = "not-a-valid-address";

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcACT_MALFORMED);
}

// Test 3: Account not found
TEST_F(GetAccountBalanceTest, ErrorAccountNotFound) {
    Json::Value params;
    params[jss::account] = "rN7n7otQDd6FczFgLdlqtyMVrn3NnrcVXs";  // Random

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcACT_NOT_FOUND);
}

// Test 4: Ledger not found
TEST_F(GetAccountBalanceTest, ErrorLedgerNotFound) {
    Json::Value params;
    params[jss::account] = alice_.human();
    params[jss::ledger_index] = 999999999;  // Non-existent

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcLGR_NOT_FOUND);
}

// Test 5: No current ledger
TEST_F(GetAccountBalanceTest, ErrorNoCurrentLedger) {
    Json::Value params;
    params[jss::account] = alice_.human();

    // Create context with no ledger
    RPC::JsonContext context = createContextWithoutLedger(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcNO_CURRENT);
}
```

***

## Input Validation Tests

Focused tests for parameter validation:

```cpp
class InputValidationTest : public ::testing::Test {
protected:
    Env env_{*this};

    RPC::JsonContext createContext(Json::Value const& params) {
        // Helper...
    }
};

// Numeric parameter validation
TEST_F(InputValidationTest, ValidateLimitParameter) {
    // Limit must be positive integer
    Json::Value params;
    params[jss::limit] = "not-a-number";

    RPC::JsonContext context = createContext(params);
    Json::Value result = doMyHandler(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcINVALID_PARAMS);
}

TEST_F(InputValidationTest, ValidateLimitBounds) {
    // Limit must be between 1 and 1000
    Json::Value params;
    params[jss::limit] = 2000;  // Too high

    RPC::JsonContext context = createContext(params);
    Json::Value result = doMyHandler(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcINVALID_PARAMS);
}

// Currency validation
TEST_F(InputValidationTest, ValidateCurrencyCode) {
    Json::Value params;
    params["currency"] = "INVALID_LONG_CODE";

    RPC::JsonContext context = createContext(params);
    Json::Value result = doMyHandler(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcINVALID_PARAMS);
}

// Amount validation
TEST_F(InputValidationTest, ValidateAmountFormat) {
    Json::Value params;
    params["amount"] = "not-valid-json-amount";

    RPC::JsonContext context = createContext(params);
    Json::Value result = doMyHandler(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcINVALID_PARAMS);
}

// Optional parameter validation
TEST_F(InputValidationTest, ValidatesOptionalParameters) {
    Json::Value params;
    params["optional_field"] = -1;  // Invalid if present

    RPC::JsonContext context = createContext(params);
    Json::Value result = doMyHandler(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcINVALID_PARAMS);
}
```

***

## Role-Based Permission Tests

Test authorization for different roles:

```cpp
class RoleBasedAccessTest : public ::testing::Test {
protected:
    Env env_{*this};
    Account account_;

    RPC::JsonContext createContextWithRole(Role role,
                                          Json::Value const& params) {
        // Create context with specified role
    }
};

// Test 1: Guest has limited access
TEST_F(RoleBasedAccessTest, GuestCanQueryPublicData) {
    Json::Value params;
    params[jss::account] = account_.human();

    RPC::JsonContext context = createContextWithRole(Role::GUEST, params);
    Json::Value result = doAccountInfo(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
}

// Test 2: User can access standard operations
TEST_F(RoleBasedAccessTest, UserCanQueryAccountInfo) {
    Json::Value params;
    params[jss::account] = account_.human();

    RPC::JsonContext context = createContextWithRole(Role::USER, params);
    Json::Value result = doAccountInfo(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
    EXPECT_TRUE(result.isMember("sequence"));
}

// Test 3: Admin gets all data
TEST_F(RoleBasedAccessTest, AdminGetsAllData) {
    Json::Value params;
    params[jss::account] = account_.human();

    RPC::JsonContext context = createContextWithRole(Role::ADMIN, params);
    Json::Value result = doAccountInfo(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
    EXPECT_TRUE(result.isMember("ledger_entry_index"));  // Admin-only field
}

// Test 4: Handler enforces role requirement
TEST_F(RoleBasedAccessTest, SubmitRequiresIdentifiedRole) {
    Json::Value params;
    params["tx_json"] = createTransaction();

    // Guest tries to submit
    RPC::JsonContext context = createContextWithRole(Role::GUEST, params);
    Json::Value result = doSubmit(context);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcNO_PERMISSION);
}
```

***

## Edge Case Tests

Tests for boundary conditions and unusual scenarios:

```cpp
class EdgeCaseTest : public ::testing::Test {
protected:
    Env env_{*this};
};

// Test 1: Empty account string
TEST_F(EdgeCaseTest, EmptyAccountString) {
    Json::Value params;
    params[jss::account] = "";

    RPC::JsonContext context = createContext(params);
    Json::Value result = doMyHandler(context);

    EXPECT_TRUE(result.isMember(jss::error));
}

// Test 2: Very large balance
TEST_F(EdgeCaseTest, LargeBalance) {
    auto richAccount = env_.fund(drops("99999999999999999"));
    env_.close();

    Json::Value params;
    params[jss::account] = richAccount.human();

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
    EXPECT_TRUE(result.isMember("balance"));
}

// Test 3: Zero balance
TEST_F(EdgeCaseTest, ZeroBalance) {
    // Create account with minimum reserve
    auto minAccount = env_.fund(drops(20000000));  // Base reserve
    env_.close();

    Json::Value params;
    params[jss::account] = minAccount.human();

    RPC::JsonContext context = createContext(params);
    Json::Value result = doGetAccountBalance(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
}

// Test 4: Maximum parameter values
TEST_F(EdgeCaseTest, MaximumLimit) {
    Json::Value params;
    params[jss::limit] = 1000;  // Maximum allowed

    RPC::JsonContext context = createContext(params);
    Json::Value result = doMyHandler(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
}

// Test 5: Minimum parameter values
TEST_F(EdgeCaseTest, MinimumLimit) {
    Json::Value params;
    params[jss::limit] = 1;  // Minimum allowed

    RPC::JsonContext context = createContext(params);
    Json::Value result = doMyHandler(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
}

// Test 6: Special ledger indices
TEST_F(EdgeCaseTest, SpecialLedgerIndices) {
    Account account = env_.fund(drops(1000000));
    env_.close();

    Json::Value params;
    params[jss::account] = account.human();

    // Test with "validated"
    params[jss::ledger_index] = "validated";
    RPC::JsonContext ctx1 = createContext(params);
    Json::Value result1 = doMyHandler(ctx1);
    EXPECT_EQ(result1[jss::status].asString(), jss::success);

    // Test with "current"
    params[jss::ledger_index] = "current";
    RPC::JsonContext ctx2 = createContext(params);
    Json::Value result2 = doMyHandler(ctx2);
    EXPECT_EQ(result2[jss::status].asString(), jss::success);
}

// Test 7: Concurrent requests
TEST_F(EdgeCaseTest, ConcurrentRequests) {
    Account account = env_.fund(drops(1000000));
    env_.close();

    // Simulate multiple concurrent requests
    std::vector<std::thread> threads;

    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([this, &account]() {
            Json::Value params;
            params[jss::account] = account.human();

            RPC::JsonContext context = createContext(params);
            Json::Value result = doMyHandler(context);

            ASSERT_EQ(result[jss::status].asString(), jss::success);
        });
    }

    for (auto& thread : threads) {
        thread.join();
    }
}
```

***

## Integration Tests

Tests that verify end-to-end functionality:

```cpp
class IntegrationTest : public ::testing::Test {
protected:
    Env env_{*this};
};

// Test 1: Complete transaction flow
TEST_F(IntegrationTest, CompleteTransactionQueryFlow) {
    // Setup accounts
    auto alice = env_.fund(drops(1000000000));
    auto bob = env_.fund(drops(1000000000));
    env_.close();

    // Send payment
    env_(pay(alice, bob, drops(100000)));
    env_.close();

    // Query account info
    Json::Value params;
    params[jss::account] = alice.human();

    RPC::JsonContext context = createContext(params);
    Json::Value result = doAccountInfo(context);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
    EXPECT_EQ(
        result["sequence"].asUInt(),
        2  // Incremented after transaction
    );
}

// Test 2: Multiple operations in sequence
TEST_F(IntegrationTest, MultipleOperationsSequence) {
    auto account = env_.fund(drops(1000000000));
    env_.close();

    for (int i = 0; i < 5; ++i) {
        Json::Value params;
        params[jss::account] = account.human();

        RPC::JsonContext context = createContext(params);
        Json::Value result = doMyHandler(context);

        EXPECT_EQ(result[jss::status].asString(), jss::success);
    }
}

// Test 3: Ledger state consistency
TEST_F(IntegrationTest, LedgerStateConsistency) {
    auto account = env_.fund(drops(1000000000));
    env_.close();

    // Query on validated ledger
    Json::Value params1;
    params1[jss::account] = account.human();
    params1[jss::ledger_index] = "validated";

    RPC::JsonContext ctx1 = createContext(params1);
    Json::Value result1 = doMyHandler(ctx1);

    // Query on current ledger
    Json::Value params2;
    params2[jss::account] = account.human();
    params2[jss::ledger_index] = "current";

    RPC::JsonContext ctx2 = createContext(params2);
    Json::Value result2 = doMyHandler(ctx2);

    // Results should be consistent for most fields
    EXPECT_EQ(result1[jss::account].asString(),
              result2[jss::account].asString());
}
```

***

## Test Execution and Coverage

### Running Tests

```bash
# Run all RPC handler tests
./rippled --unittest --unittest-rpc

# Run specific test file
./rippled --unittest --unittest-testcase=MyHandlerTest

# Run with verbose output
./rippled --unittest --unittest-verbose
```

### Code Coverage Analysis

```bash
# Compile with coverage instrumentation
cmake -DCMAKE_BUILD_TYPE=Debug -DCOVERAGE=ON ..
make

# Run tests
./rippled --unittest

# Generate coverage report
gcov src/xrpld/rpc/handlers/MyHandler.cpp
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_html
```

### Coverage Target

Aim for **minimum 80% code coverage**:

* All success paths
* All error conditions
* All role-based branches
* Edge cases

***

## Best Practices for RPC Handler Testing

### ✅ DO

* **Test all error codes** — Every possible error should be tested
* **Test with multiple roles** — Verify authorization at each level
* **Use descriptive test names** — Make tests self-documenting
* **Test both boundaries** — Minimum and maximum values
* **Mock external dependencies** — Isolate the code under test
* **Test concurrent access** — Ensure thread safety
* **Test with real ledger data** — Use JTX to create realistic scenarios

### ❌ DON'T

* **Skip error path tests** — Error handling is as important as success
* **Test implementation details** — Test behavior, not internals
* **Use hardcoded test data** — Use factories and builders
* **Ignore resource limits** — Test DoS prevention
* **Test manually** — Automate all tests
* **Skip regression tests** — Add test for every bug found

***

## Example: Complete Test Suite

```cpp
#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/handlers/Handlers.h>
#include <test/jtx.h>
#include <gtest/gtest.h>

namespace ripple {
namespace test {

class GetAccountBalanceTest : public ::testing::Test {
protected:
    Env env_{*this};
    Account alice_;
    Account bob_;

    void SetUp() override {
        alice_ = env_.fund(drops(1000000000));
        bob_ = env_.fund(drops(500000000));
        env_.close();
    }

    RPC::JsonContext createContext(
        Json::Value const& params,
        Role role = Role::USER)
    {
        return RPC::JsonContext{
            params,
            env_.app(),
            *consumer_,
            role,
            env_.current(),
            env_.app().getOPs(),
            env_.app().getLedgerMaster(),
            1
        };
    }
};

// Happy path tests
TEST_F(GetAccountBalanceTest, ReturnsBalance) {
    Json::Value params;
    params[jss::account] = alice_.human();

    RPC::JsonContext ctx = createContext(params);
    Json::Value result = doGetAccountBalance(ctx);

    EXPECT_EQ(result[jss::status].asString(), jss::success);
    EXPECT_EQ(result["balance"].asString(), "1000000000");
}

// Error tests
TEST_F(GetAccountBalanceTest, ErrorMissingAccount) {
    Json::Value params;

    RPC::JsonContext ctx = createContext(params);
    Json::Value result = doGetAccountBalance(ctx);

    EXPECT_EQ(result[jss::error_code].asInt(), rpcINVALID_PARAMS);
}

// Role-based tests
TEST_F(GetAccountBalanceTest, GuestHasLimitedAccess) {
    Json::Value params;
    params[jss::account] = alice_.human();

    RPC::JsonContext ctx = createContext(params, Role::GUEST);
    Json::Value result = doGetAccountBalance(ctx);

    // Guest can see balance but not sequence
    EXPECT_TRUE(result.isMember("balance"));
    EXPECT_FALSE(result.isMember("sequence"));
}

} // namespace test
} // namespace ripple
```

***

### Conclusion

Thorough testing is non-negotiable for production-quality RPC handlers. By combining unit tests with integration tests, using descriptive names, testing all code paths including error conditions, and verifying role-based authorization, you build confidence that your handler will behave correctly under all circumstances. The investment in comprehensive test coverage pays dividends in reduced bugs, easier refactoring, and faster debugging when issues arise.

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xrpl-commons.org/core-dev-bootcamp/module07/testing-rpc-handlers.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
