Testing
This guide covers testing React components that use the Privacy Boost SDK.Mocking the SDK
Create Mock SDK
Copy
// test/mocks/sdk.ts
import { PrivacyBoost } from '@testinprod-io/privacy-boost';
export function createMockSDK(overrides = {}): Partial<PrivacyBoost> {
return {
auth: {
connect: jest.fn().mockResolvedValue({ address: '0x123', chainId: 1 }),
disconnect: jest.fn().mockResolvedValue(undefined),
login: jest.fn().mockResolvedValue({ privacyAddress: '0x04...', mpk: '0x...' }),
logout: jest.fn().mockResolvedValue(undefined),
isConnected: jest.fn().mockReturnValue(false),
isAuthenticated: jest.fn().mockReturnValue(false),
getAddress: jest.fn().mockReturnValue(null),
getPrivacyAddress: jest.fn().mockReturnValue(null),
getMpk: jest.fn().mockReturnValue(null),
exportSession: jest.fn().mockReturnValue({}),
importSession: jest.fn().mockResolvedValue(undefined),
getSessionInfo: jest.fn().mockResolvedValue(null),
...overrides,
},
vault: {
deposit: jest.fn().mockResolvedValue({ txHash: '0x...', commitment: '0x...' }),
withdraw: jest.fn().mockResolvedValue({ txHash: '0x...' }),
send: jest.fn().mockResolvedValue({ txHash: '0x...' }),
getBalance: jest.fn().mockResolvedValue(0n),
getAllBalances: jest.fn().mockResolvedValue([]),
syncBalance: jest.fn().mockResolvedValue(undefined),
syncAllBalances: jest.fn().mockResolvedValue(undefined),
...overrides,
},
contacts: {
getContacts: jest.fn().mockReturnValue([]),
addContact: jest.fn().mockResolvedValue({ id: '1', name: 'Test' }),
removeContact: jest.fn().mockResolvedValue(undefined),
updateContact: jest.fn().mockResolvedValue({ id: '1', name: 'Updated' }),
findByAddress: jest.fn().mockReturnValue(undefined),
search: jest.fn().mockReturnValue([]),
...overrides,
},
...overrides,
} as unknown as PrivacyBoost;
}
Mock Provider
Copy
// test/mocks/MockProvider.tsx
import React from 'react';
import { PrivacyBoostProvider } from '@testinprod-io/privacy-boost-react';
import { createMockSDK } from './sdk';
interface MockProviderProps {
children: React.ReactNode;
sdkOverrides?: Partial<ReturnType<typeof createMockSDK>>;
}
export function MockPrivacyBoostProvider({
children,
sdkOverrides,
}: MockProviderProps) {
const mockSDK = createMockSDK(sdkOverrides);
return (
<PrivacyBoostProvider sdk={mockSDK as any}>
{children}
</PrivacyBoostProvider>
);
}
Testing Components
Testing useAuth
Copy
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
import { WalletButton } from '../components/WalletButton';
describe('WalletButton', () => {
it('shows connect button when not authenticated', () => {
render(
<MockPrivacyBoostProvider>
<WalletButton />
</MockPrivacyBoostProvider>
);
expect(screen.getByText('Connect Wallet')).toBeInTheDocument();
});
it('shows address when authenticated', () => {
const mockSDK = createMockSDK({
auth: {
isAuthenticated: () => true,
getPrivacyAddress: () => '0x04abc123...',
},
});
render(
<MockPrivacyBoostProvider sdkOverrides={mockSDK}>
<WalletButton />
</MockPrivacyBoostProvider>
);
expect(screen.getByText(/0x04abc123/)).toBeInTheDocument();
});
it('calls authenticate on click', async () => {
const mockConnect = jest.fn().mockResolvedValue({ address: '0x123' });
const mockLogin = jest.fn().mockResolvedValue({ privacyAddress: '0x04...' });
render(
<MockPrivacyBoostProvider
sdkOverrides={{
auth: { connect: mockConnect, login: mockLogin },
}}
>
<WalletButton />
</MockPrivacyBoostProvider>
);
fireEvent.click(screen.getByText('Connect Wallet'));
await waitFor(() => {
expect(mockConnect).toHaveBeenCalled();
});
});
});
Testing useVault
Copy
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
import { DepositForm } from '../components/DepositForm';
describe('DepositForm', () => {
it('calls deposit with correct params', async () => {
const mockDeposit = jest.fn().mockResolvedValue({ txHash: '0x...' });
render(
<MockPrivacyBoostProvider
sdkOverrides={{
vault: { deposit: mockDeposit },
auth: { isAuthenticated: () => true },
}}
>
<DepositForm tokenAddress="0xtoken" />
</MockPrivacyBoostProvider>
);
const input = screen.getByPlaceholderText('Amount');
fireEvent.change(input, { target: { value: '1.5' } });
fireEvent.click(screen.getByText('Deposit'));
await waitFor(() => {
expect(mockDeposit).toHaveBeenCalledWith({
tokenAddress: '0xtoken',
amount: '1.5',
onProgress: expect.any(Function),
});
});
});
it('shows error on failure', async () => {
const mockDeposit = jest.fn().mockRejectedValue(new Error('Insufficient balance'));
render(
<MockPrivacyBoostProvider
sdkOverrides={{
vault: { deposit: mockDeposit },
auth: { isAuthenticated: () => true },
}}
>
<DepositForm tokenAddress="0xtoken" />
</MockPrivacyBoostProvider>
);
fireEvent.change(screen.getByPlaceholderText('Amount'), {
target: { value: '100' },
});
fireEvent.click(screen.getByText('Deposit'));
await waitFor(() => {
expect(screen.getByText(/Insufficient balance/)).toBeInTheDocument();
});
});
});
Testing useBalances
Copy
import { render, screen } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
import { BalanceList } from '../components/BalanceList';
describe('BalanceList', () => {
it('displays balances', () => {
const mockBalances = [
{
tokenAddress: '0x1',
symbol: 'USDC',
shielded: 1000000n,
wallet: 500000n,
decimals: 6,
formattedShielded: '1.0',
formattedWallet: '0.5',
},
];
// Mock the store directly
jest.mock('@testinprod-io/privacy-boost', () => ({
useBalanceStore: () => ({
balances: new Map([['0x1', mockBalances[0]]]),
}),
}));
render(
<MockPrivacyBoostProvider>
<BalanceList />
</MockPrivacyBoostProvider>
);
expect(screen.getByText('USDC')).toBeInTheDocument();
expect(screen.getByText('1.0')).toBeInTheDocument();
});
});
Testing Hooks
Custom Hook Testing
Copy
import { renderHook, act } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
import { useDepositFlow } from '../hooks/useDepositFlow';
describe('useDepositFlow', () => {
it('handles deposit flow', async () => {
const mockDeposit = jest.fn().mockResolvedValue({ txHash: '0x...' });
const wrapper = ({ children }) => (
<MockPrivacyBoostProvider
sdkOverrides={{
vault: { deposit: mockDeposit },
auth: { isAuthenticated: () => true },
}}
>
{children}
</MockPrivacyBoostProvider>
);
const { result } = renderHook(() => useDepositFlow(), { wrapper });
expect(result.current.loading).toBe(false);
await act(async () => {
await result.current.deposit('0xtoken', '1.0');
});
expect(mockDeposit).toHaveBeenCalled();
expect(result.current.txHash).toBe('0x...');
});
});
Mocking window.ethereum
Copy
beforeEach(() => {
// Mock window.ethereum
Object.defineProperty(window, 'ethereum', {
value: {
isMetaMask: true,
request: jest.fn(),
on: jest.fn(),
removeListener: jest.fn(),
},
writable: true,
});
});
afterEach(() => {
// @ts-ignore
delete window.ethereum;
});
Test Utilities
Copy
// test/utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
function customRender(
ui: React.ReactElement,
sdkOverrides?: any,
options?: Omit<RenderOptions, 'wrapper'>
) {
return render(ui, {
wrapper: ({ children }) => (
<MockPrivacyBoostProvider sdkOverrides={sdkOverrides}>
{children}
</MockPrivacyBoostProvider>
),
...options,
});
}
export * from '@testing-library/react';
export { customRender as render };
Best Practices
- Mock at the SDK level, not individual hooks
- Test user interactions, not implementation details
- Use async utilities (
waitFor,findBy*) for async operations - Clean up mocks between tests
- Test error states as well as success states