Testing

Testing your LaserEyes integration is crucial for ensuring reliability and correctness. This page covers strategies and best practices for testing LaserEyes applications.

Testing Challenges

Testing Bitcoin wallet applications presents unique challenges:

  • Wallet Dependencies: Tests need to interact with wallet extensions
  • Blockchain Interactions: Tests may need to interact with the blockchain
  • Network Variability: Blockchain networks can have variable performance
  • Test Environment: Setting up a test environment can be complex
  • Real Funds: Testing with real funds is risky

Testing Approaches

To address these challenges, we recommend a multi-layered testing approach:

  • Unit Testing: Test individual functions and components in isolation
  • Integration Testing: Test interactions between components
  • Mocking: Mock wallet and blockchain interactions
  • Testnet Testing: Use Bitcoin Testnet for end-to-end tests
  • Manual Testing: Perform manual tests for critical flows

Unit Testing

Unit tests focus on testing individual functions and components in isolation:

format-bitcoin-amount.test.ts
// Example unit test for a utility function
import { formatBitcoinAmount } from '../utils/formatters'

describe('formatBitcoinAmount', () => {
  test('formats satoshis as BTC with 8 decimal places', () => {
    expect(formatBitcoinAmount('1000000')).toBe('0.01000000')
    expect(formatBitcoinAmount('123456789')).toBe('1.23456789')
    expect(formatBitcoinAmount('0')).toBe('0.00000000')
  })
  
  test('handles string and number inputs', () => {
    expect(formatBitcoinAmount(1000000)).toBe('0.01000000')
    expect(formatBitcoinAmount('1000000')).toBe('0.01000000')
  })
  
  test('handles invalid inputs', () => {
    expect(formatBitcoinAmount('')).toBe('0.00000000')
    expect(formatBitcoinAmount('invalid')).toBe('0.00000000')
    expect(formatBitcoinAmount(null)).toBe('0.00000000')
    expect(formatBitcoinAmount(undefined)).toBe('0.00000000')
  })
})

For React components, you can use testing libraries like React Testing Library:

bitcoin-address-input.test.tsx
// Example unit test for a React component
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { BitcoinAddressInput } from '../components/BitcoinAddressInput'

describe('BitcoinAddressInput', () => {
  test('renders input field', () => {
    render(<BitcoinAddressInput value="" onChange={() => {}} />)
    
    const input = screen.getByPlaceholderText('Enter Bitcoin address')
    expect(input).toBeInTheDocument()
  })
  
  test('calls onChange when input changes', async () => {
    const handleChange = jest.fn()
    render(<BitcoinAddressInput value="" onChange={handleChange} />)
    
    const input = screen.getByPlaceholderText('Enter Bitcoin address')
    await userEvent.type(input, 'bc1q...')
    
    expect(handleChange).toHaveBeenCalled()
  })
  
  test('shows error for invalid address', () => {
    render(<BitcoinAddressInput value="invalid" onChange={() => {}} />)
    
    const errorMessage = screen.getByText('Invalid Bitcoin address')
    expect(errorMessage).toBeInTheDocument()
  })
})

Mocking LaserEyes

To test components that use LaserEyes, you'll need to mock the LaserEyes hooks and providers:

wallet-connect.test.tsx
// Create a mock for useLaserEyes hook
jest.mock('@omnisat/lasereyes-react', () => ({
  useLaserEyes: () => ({
    connect: jest.fn(),
    disconnect: jest.fn(),
    connected: false,
    address: '',
    balance: '0',
    sendBTC: jest.fn(),
    getInscriptions: jest.fn(),
    getMetaBalances: jest.fn(),
    // Add other methods and properties as needed
  }),
  LaserEyesProvider: ({ children }) => children,
}))

// Example test for a component that uses useLaserEyes
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useLaserEyes } from '@omnisat/lasereyes-react'
import { WalletConnect } from '../components/WalletConnect'

describe('WalletConnect', () => {
  beforeEach(() => {
    // Reset mocks before each test
    jest.clearAllMocks()
  })
  
  test('renders connect button when not connected', () => {
    // Override the mock for this test
    const mockUseLaserEyes = useLaserEyes as jest.Mock
    mockUseLaserEyes.mockReturnValue({
      connect: jest.fn(),
      disconnect: jest.fn(),
      connected: false,
      address: '',
      balance: '0',
    })
    
    render(<WalletConnect />)
    
    const connectButton = screen.getByText('Connect Wallet')
    expect(connectButton).toBeInTheDocument()
  })
  
  test('renders address and disconnect button when connected', () => {
    // Override the mock for this test
    const mockUseLaserEyes = useLaserEyes as jest.Mock
    mockUseLaserEyes.mockReturnValue({
      connect: jest.fn(),
      disconnect: jest.fn(),
      connected: true,
      address: 'bc1qtest...',
      balance: '1000000',
    })
    
    render(<WalletConnect />)
    
    const addressElement = screen.getByText(/bc1qtest.../)
    const disconnectButton = screen.getByText('Disconnect')
    
    expect(addressElement).toBeInTheDocument()
    expect(disconnectButton).toBeInTheDocument()
  })
  
  test('calls connect when connect button is clicked', async () => {
    const mockConnect = jest.fn()
    
    // Override the mock for this test
    const mockUseLaserEyes = useLaserEyes as jest.Mock
    mockUseLaserEyes.mockReturnValue({
      connect: mockConnect,
      disconnect: jest.fn(),
      connected: false,
      address: '',
      balance: '0',
    })
    
    render(<WalletConnect />)
    
    const connectButton = screen.getByText('Connect Wallet')
    await userEvent.click(connectButton)
    
    expect(mockConnect).toHaveBeenCalled()
  })
})

Integration Testing

Integration tests focus on testing interactions between components:

wallet-page-integration.test.tsx
// Example integration test for a wallet flow
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LaserEyesProvider } from '@omnisat/lasereyes-react'
import { WalletPage } from '../pages/WalletPage'

// Mock the LaserEyesClient
jest.mock('@omnisat/lasereyes-core', () => {
  const originalModule = jest.requireActual('@omnisat/lasereyes-core')
  
  return {
    ...originalModule,
    LaserEyesClient: jest.fn().mockImplementation(() => ({
      initialize: jest.fn(),
      connect: jest.fn().mockResolvedValue({ address: 'bc1qtest...', publicKey: 'test' }),
      disconnect: jest.fn().mockResolvedValue(undefined),
      getBalance: jest.fn().mockResolvedValue('1000000'),
      getInscriptions: jest.fn().mockResolvedValue([]),
      sendBTC: jest.fn().mockResolvedValue('txid123'),
      // Add other methods as needed
    })),
  }
})

// Create a wrapper with the LaserEyesProvider
const renderWithProvider = (ui) => {
  return render(
    <LaserEyesProvider config={{ network: 'testnet' }}>
      {ui}
    </LaserEyesProvider>
  )
}

describe('WalletPage Integration', () => {
  test('complete wallet flow: connect, view balance, send transaction', async () => {
    renderWithProvider(<WalletPage />)
    
    // Step 1: Connect wallet
    const connectButton = screen.getByText('Connect Wallet')
    await userEvent.click(connectButton)
    
    // Wait for connection to complete
    await waitFor(() => {
      expect(screen.getByText(/bc1qtest.../)).toBeInTheDocument()
    })
    
    // Step 2: Check balance
    expect(screen.getByText('0.01000000 BTC')).toBeInTheDocument()
    
    // Step 3: Fill send form
    const recipientInput = screen.getByLabelText('Recipient')
    const amountInput = screen.getByLabelText('Amount')
    const sendButton = screen.getByText('Send BTC')
    
    await userEvent.type(recipientInput, 'bc1qrecipient...')
    await userEvent.type(amountInput, '0.005')
    await userEvent.click(sendButton)
    
    // Step 4: Verify transaction success
    await waitFor(() => {
      expect(screen.getByText(/Transaction sent/)).toBeInTheDocument()
      expect(screen.getByText(/txid123/)).toBeInTheDocument()
    })
  })
})

Testing with Testnet

For end-to-end testing, you can use Bitcoin Testnet:

testnet-testing.tsx
// Configure LaserEyes to use Testnet for testing
import { LaserEyesProvider } from '@omnisat/lasereyes-react'
import { TESTNET } from '@omnisat/lasereyes-core'
import { render } from '@testing-library/react'
import { App } from '../App'

// Create a wrapper with the LaserEyesProvider configured for Testnet
const renderWithTestnet = (ui) => {
  return render(
    <LaserEyesProvider
      config={{ 
        network: TESTNET,
        dataSources: {
          // Use test API keys or public Testnet APIs
          maestro: {
            apiKey: process.env.TEST_MAESTRO_API_KEY,
          },
          mempool: {
            url: 'https://mempool.space/testnet/api',
          }
        }
      }}
    >
      {ui}
    </LaserEyesProvider>
  )
}

// Now you can use this in your tests
test('end-to-end test with Testnet', async () => {
  renderWithTestnet(<App />)
  
  // Your test code here
  // Note: This will require a wallet with Testnet support
  // and Testnet coins for full end-to-end testing
})

Testnet Testing Considerations

When testing with Testnet, keep in mind:
  • You'll need a wallet that supports Testnet
  • You'll need Testnet coins (available from faucets)
  • Testnet can be unstable at times
  • These tests are more like integration tests than unit tests

Mocking Data Providers

For testing data provider interactions, you can mock the DataSourceManager:

mock-data-provider.ts
// Mock the DataSourceManager
jest.mock('@omnisat/lasereyes-core', () => {
  const originalModule = jest.requireActual('@omnisat/lasereyes-core')
  
  return {
    ...originalModule,
    DataSourceManager: jest.fn().mockImplementation(() => ({
      getBalance: jest.fn().mockResolvedValue('1000000'),
      getUtxos: jest.fn().mockResolvedValue([
        {
          txid: 'txid1',
          vout: 0,
          value: '500000',
          status: { confirmed: true }
        },
        {
          txid: 'txid2',
          vout: 1,
          value: '500000',
          status: { confirmed: true }
        }
      ]),
      getInscriptions: jest.fn().mockResolvedValue([
        {
          id: 'inscription1',
          number: 1,
          contentType: 'image/png',
          content: 'data:image/png;base64,...'
        }
      ]),
      getMetaBalances: jest.fn().mockImplementation((type) => {
        if (type === 'brc20') {
          return Promise.resolve([
            { ticker: 'ORDI', balance: '100' },
            { ticker: 'SATS', balance: '200' }
          ])
        } else if (type === 'runes') {
          return Promise.resolve([
            { id: 'rune1', symbol: 'RUNE1', balance: '100' }
          ])
        }
        return Promise.resolve([])
      }),
      // Add other methods as needed
    })),
  }
})

Testing Error Handling

It's important to test how your application handles errors:

error-handling.test.tsx
// Example test for error handling
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useLaserEyes } from '@omnisat/lasereyes-react'
import { SendBTCForm } from '../components/SendBTCForm'

// Mock the useLaserEyes hook
jest.mock('@omnisat/lasereyes-react', () => ({
  useLaserEyes: jest.fn(),
}))

describe('SendBTCForm Error Handling', () => {
  test('displays error when transaction fails', async () => {
    // Mock the sendBTC function to throw an error
    const mockSendBTC = jest.fn().mockRejectedValue({
      code: 'INSUFFICIENT_FUNDS',
      message: 'Insufficient funds for this transaction'
    })
    
    // Set up the mock return value
    const mockUseLaserEyes = useLaserEyes as jest.Mock
    mockUseLaserEyes.mockReturnValue({
      connected: true,
      address: 'bc1qtest...',
      balance: '1000',
      sendBTC: mockSendBTC,
    })
    
    render(<SendBTCForm />)
    
    // Fill the form
    const recipientInput = screen.getByLabelText('Recipient')
    const amountInput = screen.getByLabelText('Amount')
    const sendButton = screen.getByText('Send')
    
    await userEvent.type(recipientInput, 'bc1qrecipient...')
    await userEvent.type(amountInput, '0.005')
    await userEvent.click(sendButton)
    
    // Verify error message is displayed
    await waitFor(() => {
      expect(screen.getByText(/Insufficient funds for this transaction/)).toBeInTheDocument()
    })
  })
  
  test('displays error for invalid address', async () => {
    // Mock the sendBTC function to throw an error
    const mockSendBTC = jest.fn().mockRejectedValue({
      code: 'INVALID_ADDRESS',
      message: 'Invalid Bitcoin address'
    })
    
    // Set up the mock return value
    const mockUseLaserEyes = useLaserEyes as jest.Mock
    mockUseLaserEyes.mockReturnValue({
      connected: true,
      address: 'bc1qtest...',
      balance: '1000000',
      sendBTC: mockSendBTC,
    })
    
    render(<SendBTCForm />)
    
    // Fill the form with an invalid address
    const recipientInput = screen.getByLabelText('Recipient')
    const amountInput = screen.getByLabelText('Amount')
    const sendButton = screen.getByText('Send')
    
    await userEvent.type(recipientInput, 'invalid')
    await userEvent.type(amountInput, '0.005')
    await userEvent.click(sendButton)
    
    // Verify error message is displayed
    await waitFor(() => {
      expect(screen.getByText(/Invalid Bitcoin address/)).toBeInTheDocument()
    })
  })
})

Testing Best Practices

Follow these best practices for testing LaserEyes applications:

  • Test in Isolation: Use mocks to isolate components and functions
  • Test User Flows: Focus on testing complete user flows
  • Test Error Handling: Ensure your application handles errors gracefully
  • Use Test Doubles: Create test doubles (mocks, stubs, fakes) for external dependencies
  • Test Edge Cases: Test boundary conditions and edge cases
  • Keep Tests Fast: Optimize tests for speed to encourage frequent testing
  • Use CI/CD: Integrate tests into your CI/CD pipeline

Testing Tools

Here are some recommended tools for testing LaserEyes applications:

ToolTypeUse Case
JestTest RunnerRunning unit and integration tests
React Testing LibraryComponent TestingTesting React components
Mock Service WorkerAPI MockingMocking API responses
CypressEnd-to-End TestingTesting complete user flows
PlaywrightEnd-to-End TestingTesting across multiple browsers
StorybookComponent DevelopmentDeveloping and testing components in isolation

Next Steps

Now that you understand testing strategies for LaserEyes, you can explore related topics: