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:
// 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:
// 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:
// 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:
// 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:
// 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
- 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 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:
// 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:
Tool | Type | Use Case |
---|---|---|
Jest | Test Runner | Running unit and integration tests |
React Testing Library | Component Testing | Testing React components |
Mock Service Worker | API Mocking | Mocking API responses |
Cypress | End-to-End Testing | Testing complete user flows |
Playwright | End-to-End Testing | Testing across multiple browsers |
Storybook | Component Development | Developing and testing components in isolation |
Next Steps
Now that you understand testing strategies for LaserEyes, you can explore related topics:
- Error Handling - Learn how to handle errors effectively
- Security Considerations - Understand security best practices
- Performance Optimization - Optimize your application's performance
- Best Practices - General best practices for LaserEyes