Testing Strategies | XRPL APIs & Integration | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
advanced50 min

Testing Strategies

Learning Objectives

Structure test suites for XRPL integrations with appropriate coverage

Mock XRPL responses for fast, isolated unit testing

Test against testnet for realistic integration validation

Simulate failure scenarios to verify error handling

Load test to validate performance under stress

                 /\
                /  \
               / E2E \         ← Real transactions on testnet
              /________\          Slowest, most realistic
             /          \
            / Integration \    ← API calls to testnet
           /______________\       Moderate speed
          /                \
         /    Unit Tests    \  ← Mocked responses
        /____________________\    Fastest, most isolated
tests/
├── unit/
│   ├── validation.test.js      # Input validation
│   ├── amount-parsing.test.js  # Currency handling
│   ├── error-handling.test.js  # Error classification
│   └── payment-logic.test.js   # Business logic
├── integration/
│   ├── connection.test.js      # WebSocket handling
│   ├── queries.test.js         # API queries
│   └── transactions.test.js    # Transaction submission
├── e2e/
│   ├── payment-flow.test.js    # Complete payment scenarios
│   └── trading-flow.test.js    # DEX operations
├── load/
│   └── throughput.test.js      # Performance under load
└── mocks/
    └── xrpl-responses.js       # Shared mock data

class MockXRPLClient {
  constructor() {
    this.responses = new Map()
    this.calls = []
    this.connected = false
  }

// Setup expected responses
mockResponse(command, response) {
this.responses.set(command, response)
}

mockResponseForAccount(command, account, response) {
const key = ${command}:${account}
this.responses.set(key, response)
}

async connect() {
this.connected = true
}

async disconnect() {
this.connected = false
}

isConnected() {
return this.connected
}

async request(request) {
this.calls.push(request)

// Check for account-specific mock
const accountKey = ${request.command}:${request.account}
if (this.responses.has(accountKey)) {
return { result: this.responses.get(accountKey) }
}

// Check for command mock
if (this.responses.has(request.command)) {
const response = this.responses.get(request.command)
if (typeof response === 'function') {
return { result: response(request) }
}
return { result: response }
}

throw new Error(No mock for command: ${request.command})
}

getCallsFor(command) {
return this.calls.filter(c => c.command === command)
}

reset() {
this.calls = []
}
}
```

const { describe, it, expect, beforeEach } = require('jest')
const { PaymentValidator } = require('../src/payment-validator')
const { MockXRPLClient } = require('./mocks/xrpl-client')

describe('PaymentValidator', () => {
let client
let validator

beforeEach(() => {
client = new MockXRPLClient()
validator = new PaymentValidator(client)
})

describe('validateAddress', () => {
it('accepts valid addresses', () => {
const valid = 'rN7n3473SaZBCG4dFL83w7a1RXtXtbk2D9'
expect(() => validator.validateAddress(valid)).not.toThrow()
})

it('rejects invalid addresses', () => {
expect(() => validator.validateAddress('invalid')).toThrow('Invalid address')
expect(() => validator.validateAddress('')).toThrow('required')
expect(() => validator.validateAddress(null)).toThrow('required')
})

it('rejects addresses with wrong prefix', () => {
expect(() => validator.validateAddress('xN7n3473SaZBCG4dFL83w7a1RXtXtbk2D9'))
.toThrow('must start with')
})
})

describe('validateAmount', () => {
it('accepts valid XRP amounts', () => {
expect(validator.validateAmount('100')).toBe(100)
expect(validator.validateAmount('0.000001')).toBe(0.000001)
expect(validator.validateAmount(50)).toBe(50)
})

it('rejects invalid amounts', () => {
expect(() => validator.validateAmount(-1)).toThrow('positive')
expect(() => validator.validateAmount(0)).toThrow('positive')
expect(() => validator.validateAmount('abc')).toThrow('valid number')
})

it('rejects amounts with too many decimals', () => {
expect(() => validator.validateAmount('1.0000001'))
.toThrow('decimal places')
})
})

describe('checkSufficientBalance', async () => {
it('returns true when balance is sufficient', async () => {
client.mockResponse('account_info', {
account_data: {
Balance: '100000000', // 100 XRP
OwnerCount: 0
}
})
client.mockResponse('server_info', {
info: {
validated_ledger: {
reserve_base_xrp: 10,
reserve_inc_xrp: 2
}
}
})

const result = await validator.checkSufficientBalance('rAddr...', 50)
expect(result.sufficient).toBe(true)
expect(result.available).toBe(90) // 100 - 10 reserve
})

it('returns false when balance is insufficient', async () => {
client.mockResponse('account_info', {
account_data: {
Balance: '15000000', // 15 XRP
OwnerCount: 0
}
})
client.mockResponse('server_info', {
info: {
validated_ledger: {
reserve_base_xrp: 10,
reserve_inc_xrp: 2
}
}
})

const result = await validator.checkSufficientBalance('rAddr...', 10)
expect(result.sufficient).toBe(false)
expect(result.available).toBe(5)
})
})
})
```

describe('PartialPaymentDetector', () => {
  const detector = require('../src/partial-payment-detector')

it('detects partial payment flag', () => {
const tx = { Flags: 0x00020000 }
expect(detector.isPartialPayment(tx)).toBe(true)
})

it('returns false when flag not set', () => {
const tx = { Flags: 0 }
expect(detector.isPartialPayment(tx)).toBe(false)
})

it('correctly extracts delivered_amount for XRP', () => {
const tx = {
TransactionType: 'Payment',
Amount: '100000000',
meta: {
TransactionResult: 'tesSUCCESS',
delivered_amount: '50000000' // Partial payment
}
}

const delivered = detector.getDeliveredAmount(tx)
expect(delivered.value).toBe(50) // 50 XRP, not 100
expect(delivered.currency).toBe('XRP')
})

it('rejects partial payment without delivered_amount', () => {
const tx = {
TransactionType: 'Payment',
Amount: '100000000',
Flags: 0x00020000,
meta: { TransactionResult: 'tesSUCCESS' }
// No delivered_amount
}

expect(() => detector.getDeliveredAmount(tx))
.toThrow('Cannot determine delivered amount')
})
})
```


const xrpl = require('xrpl')

class TestnetFixture {
constructor() {
this.client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
this.wallets = []
}

async setup() {
await this.client.connect()

// Create funded test wallets
this.sender = await this.createFundedWallet()
this.receiver = await this.createFundedWallet()

console.log(Sender: ${this.sender.address})
console.log(Receiver: ${this.receiver.address})
}

async createFundedWallet() {
const { wallet, balance } = await this.client.fundWallet()
this.wallets.push(wallet)
return wallet
}

async teardown() {
await this.client.disconnect()
}

async getBalance(address) {
const info = await this.client.request({
command: 'account_info',
account: address,
ledger_index: 'validated'
})
return parseInt(info.result.account_data.Balance) / 1_000_000
}
}

// Jest setup
let fixture

beforeAll(async () => {
fixture = new TestnetFixture()
await fixture.setup()
}, 30000)

afterAll(async () => {
await fixture.teardown()
})
```

describe('Payment Integration', () => {
  it('successfully sends XRP payment', async () => {
    const initialBalance = await fixture.getBalance(fixture.receiver.address)

const payment = {
TransactionType: 'Payment',
Account: fixture.sender.address,
Destination: fixture.receiver.address,
Amount: xrpl.xrpToDrops('10')
}

const result = await fixture.client.submitAndWait(payment, {
wallet: fixture.sender
})

expect(result.result.meta.TransactionResult).toBe('tesSUCCESS')

const finalBalance = await fixture.getBalance(fixture.receiver.address)
expect(finalBalance - initialBalance).toBe(10)
}, 30000)

it('fails payment to non-existent account with insufficient amount', async () => {
const nonExistent = xrpl.Wallet.generate().address

const payment = {
TransactionType: 'Payment',
Account: fixture.sender.address,
Destination: nonExistent,
Amount: xrpl.xrpToDrops('1') // Below reserve
}

const result = await fixture.client.submitAndWait(payment, {
wallet: fixture.sender
})

// Should fail because 1 XRP < 10 XRP reserve for new account
expect(result.result.meta.TransactionResult).toBe('tecNO_DST_INSUF_XRP')
}, 30000)

it('handles destination tag requirement', async () => {
// Set RequireDestTag on receiver
await fixture.client.submitAndWait({
TransactionType: 'AccountSet',
Account: fixture.receiver.address,
SetFlag: 1 // asfRequireDest
}, { wallet: fixture.receiver })

// Payment without tag should fail
const payment = {
TransactionType: 'Payment',
Account: fixture.sender.address,
Destination: fixture.receiver.address,
Amount: xrpl.xrpToDrops('5')
// No DestinationTag
}

const result = await fixture.client.submitAndWait(payment, {
wallet: fixture.sender
})

expect(result.result.meta.TransactionResult).toBe('tecDST_TAG_NEEDED')
}, 30000)
})
```


describe('Error Handling', () => {
  it('handles connection timeout', async () => {
    const slowClient = new xrpl.Client('wss://s1.ripple.com:51233')
    slowClient.connection.config.timeout = 100  // Very short timeout

await expect(slowClient.connect())
.rejects
.toThrow()
})

it('handles invalid transaction', async () => {
const payment = {
TransactionType: 'Payment',
Account: fixture.sender.address,
Destination: 'invalidaddress',
Amount: xrpl.xrpToDrops('10')
}

await expect(fixture.client.submitAndWait(payment, {
wallet: fixture.sender
})).rejects.toThrow()
})

it('handles insufficient balance', async () => {
// Try to send more than available
const info = await fixture.client.request({
command: 'account_info',
account: fixture.sender.address
})
const balance = parseInt(info.result.account_data.Balance)
const tooMuch = balance + 1000000 // More than balance

const payment = {
TransactionType: 'Payment',
Account: fixture.sender.address,
Destination: fixture.receiver.address,
Amount: tooMuch.toString()
}

const result = await fixture.client.submitAndWait(payment, {
wallet: fixture.sender
})

expect(result.result.meta.TransactionResult).toBe('tecUNFUNDED_PAYMENT')
}, 30000)
})
```

describe('Network Resilience', () => {
  it('reconnects after disconnect', async () => {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
    await client.connect()

// Simulate disconnect
client.connection.ws.close()

// Wait for reconnection logic
await sleep(5000)

// Should be able to make requests again
const response = await client.request({ command: 'server_info' })
expect(response.result.info.server_state).toBeDefined()

await client.disconnect()
}, 30000)
})
```


const { performance } = require('perf_hooks')

describe('Load Testing', () => {
it('handles concurrent requests', async () => {
const CONCURRENT = 50
const requests = []

const start = performance.now()

for (let i = 0; i < CONCURRENT; i++) {
requests.push(
fixture.client.request({
command: 'account_info',
account: fixture.sender.address,
ledger_index: 'validated'
})
)
}

const results = await Promise.allSettled(requests)

const end = performance.now()
const duration = end - start

const succeeded = results.filter(r => r.status === 'fulfilled').length
const failed = results.filter(r => r.status === 'rejected').length

console.log(${CONCURRENT} requests in ${duration.toFixed(2)}ms)
console.log(Success: ${succeeded}, Failed: ${failed})
console.log(Throughput: ${(CONCURRENT / (duration / 1000)).toFixed(2)} req/sec)

expect(succeeded).toBeGreaterThan(CONCURRENT * 0.95) // 95% success rate
}, 60000)

it('maintains performance under sustained load', async () => {
const DURATION_MS = 10000
const TARGET_RPS = 20
const INTERVAL = 1000 / TARGET_RPS

const latencies = []
const startTime = Date.now()

while (Date.now() - startTime < DURATION_MS) {
const requestStart = performance.now()

await fixture.client.request({
command: 'server_info'
})

latencies.push(performance.now() - requestStart)

await sleep(INTERVAL)
}

// Calculate percentiles
latencies.sort((a, b) => a - b)
const p50 = latencies[Math.floor(latencies.length * 0.5)]
const p95 = latencies[Math.floor(latencies.length * 0.95)]
const p99 = latencies[Math.floor(latencies.length * 0.99)]

console.log(P50: ${p50.toFixed(2)}ms, P95: ${p95.toFixed(2)}ms, P99: ${p99.toFixed(2)}ms)

expect(p95).toBeLessThan(500) // P95 under 500ms
}, 30000)
})
```



  • Unit tests with mocks for core business logic
  • Integration tests on testnet for payment flows
  • Error scenario tests for all failure modes
  • Basic load test measuring throughput and latency

Time Investment: 3-4 hours


End of Lesson 15

Key Takeaways

1

Layer your tests:

Unit tests for logic, integration tests for API, E2E for flows.

2

Mock for speed:

Use mocks in unit tests; save testnet for integration.

3

Test error paths:

Verify error handling works as designed.

4

Load test before launch:

Discover performance limits before users do.

5

Keep testnet wallets funded:

Pre-funded wallets speed up test runs. ---