Skip to main content

Overview

Testing SDK integrations requires a different approach than API testing. The SDK renders UI components, handles tokenization, and manages client-side state. This guide covers mock setup for unit tests, error state testing, integration test patterns, and CI/CD configuration.

SDK Mock Setup for Unit Tests

When unit testing your application code that integrates with the Yuno SDK, mock the SDK to isolate your business logic from the SDK internals.

Mocking the Web SDK

// __mocks__/yuno-sdk.js
const mockYuno = {
  initialize: jest.fn().mockResolvedValue(undefined),
  startCheckout: jest.fn().mockResolvedValue(undefined),
  mountSecureFields: jest.fn().mockReturnValue({
    cardNumber: { mount: jest.fn(), on: jest.fn() },
    expiry: { mount: jest.fn(), on: jest.fn() },
    cvv: { mount: jest.fn(), on: jest.fn() },
  }),
  generateToken: jest.fn().mockResolvedValue({
    token: 'mock-one-time-token',
    vaulted_token: null,
  }),
  notifyPaymentStatus: jest.fn(),
};

export default mockYuno;

// your-component.test.js
import mockYuno from './__mocks__/yuno-sdk';

jest.mock('@yuno/sdk-web', () => ({
  __esModule: true,
  default: { create: jest.fn().mockResolvedValue(mockYuno) },
}));

describe('CheckoutPage', () => {
  it('initializes SDK with checkout session', async () => {
    await initializePayment('session-123');
    expect(mockYuno.initialize).toHaveBeenCalledWith(
      expect.objectContaining({
        checkoutSession: 'session-123',
      })
    );
  });

  it('generates token on form submit', async () => {
    const token = await submitPaymentForm();
    expect(mockYuno.generateToken).toHaveBeenCalled();
    expect(token).toBe('mock-one-time-token');
  });
});

Mocking the Mobile SDK

// __mocks__/@yuno/react-native-sdk.js
export const YunoProvider = ({ children }) => children;

export const useYuno = () => ({
  initCheckout: jest.fn().mockResolvedValue(undefined),
  startPayment: jest.fn().mockResolvedValue({
    token: 'mock-mobile-token',
  }),
  isReady: true,
  error: null,
});

// checkout-screen.test.js
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import CheckoutScreen from '../CheckoutScreen';

jest.mock('@yuno/react-native-sdk');

describe('CheckoutScreen', () => {
  it('renders payment button when SDK is ready', () => {
    const { getByText } = render(<CheckoutScreen sessionId="session-123" />);
    expect(getByText('Pay Now')).toBeTruthy();
  });
});

Error State Testing

Test how your integration handles SDK errors and edge cases.

Common error scenarios

ScenarioHow to SimulateExpected Behavior
Invalid checkout sessionPass expired/invalid session IDSDK fires onError callback
Network failureDisable network in test environmentSDK shows retry option or fires error
Tokenization failureUse invalid card datagenerateToken rejects with error
Session timeoutWait >30 minutes after session creationSDK fires session expired error
SDK load failureBlock CDN in testYour fallback UI renders

Testing error callbacks

describe('Error handling', () => {
  it('handles tokenization failure gracefully', async () => {
    mockYuno.generateToken.mockRejectedValue(
      new Error('TOKENIZATION_FAILED')
    );

    const { getByText, findByText } = render(<CheckoutPage />);
    fireEvent.click(getByText('Pay Now'));

    const errorMessage = await findByText('Payment could not be processed');
    expect(errorMessage).toBeInTheDocument();
  });

  it('handles session expiry', async () => {
    const onError = jest.fn();
    mockYuno.initialize.mockImplementation(({ onError: cb }) => {
      cb({ code: 'SESSION_EXPIRED', message: 'Checkout session expired' });
    });

    await initializePayment('expired-session', { onError });
    expect(onError).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'SESSION_EXPIRED' })
    );
  });

  it('handles network timeout', async () => {
    mockYuno.generateToken.mockImplementation(
      () => new Promise((_, reject) =>
        setTimeout(() => reject(new Error('TIMEOUT')), 5000)
      )
    );

    const result = await submitPaymentWithTimeout();
    expect(result.error).toBe('Request timed out');
  });
});

Integration Test Patterns

Integration tests validate the full flow from your frontend through your backend to the Yuno sandbox API.

End-to-end with Playwright

// e2e/payment.spec.js
import { test, expect } from '@playwright/test';

test.describe('Payment flow', () => {
  test('completes card payment successfully', async ({ page }) => {
    // Navigate to checkout
    await page.goto('/checkout?orderId=test-001');

    // Wait for SDK to load
    await page.waitForSelector('[data-yuno-sdk-ready]');

    // Select card payment method
    await page.click('[data-payment-method="CARD"]');

    // Fill card details in secure fields (iframe)
    const cardFrame = page.frameLocator('[data-yuno-field="card-number"]');
    await cardFrame.locator('input').fill('4111111111111111');

    const expiryFrame = page.frameLocator('[data-yuno-field="expiry"]');
    await expiryFrame.locator('input').fill('12/30');

    const cvvFrame = page.frameLocator('[data-yuno-field="cvv"]');
    await cvvFrame.locator('input').fill('123');

    // Submit payment
    await page.click('#pay-button');

    // Wait for success page
    await expect(page).toHaveURL(/\/payment\/success/);
    await expect(page.locator('.payment-status')).toHaveText('Payment Successful');
  });

  test('handles declined card', async ({ page }) => {
    await page.goto('/checkout?orderId=test-002');
    await page.waitForSelector('[data-yuno-sdk-ready]');
    await page.click('[data-payment-method="CARD"]');

    // Use decline test card
    const cardFrame = page.frameLocator('[data-yuno-field="card-number"]');
    await cardFrame.locator('input').fill('4000000000000002');

    const expiryFrame = page.frameLocator('[data-yuno-field="expiry"]');
    await expiryFrame.locator('input').fill('12/30');

    const cvvFrame = page.frameLocator('[data-yuno-field="cvv"]');
    await cvvFrame.locator('input').fill('123');

    await page.click('#pay-button');

    // Verify error message shown
    await expect(page.locator('.payment-error')).toBeVisible();
    await expect(page.locator('.payment-error')).toContainText('declined');
  });
});

API integration tests

// integration/payment-api.test.js
const API_URL = 'https://api-sandbox.y.uno';

describe('Payment API Integration', () => {
  let checkoutSession;

  beforeAll(async () => {
    const response = await fetch(`${API_URL}/v1/checkout/sessions`, {
      method: 'POST',
      headers: {
        'public-api-key': process.env.YUNO_PUBLIC_KEY,
        'private-secret-key': process.env.YUNO_PRIVATE_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        amount: { currency: 'USD', value: 50.00 },
        country: 'CO',
        merchant_order_id: `test-${Date.now()}`,
      }),
    });
    const data = await response.json();
    checkoutSession = data.checkout_session;
  });

  it('creates a payment with valid data', async () => {
    const response = await fetch(`${API_URL}/v1/payments`, {
      method: 'POST',
      headers: {
        'public-api-key': process.env.YUNO_PUBLIC_KEY,
        'private-secret-key': process.env.YUNO_PRIVATE_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        checkout_session: checkoutSession,
        payment_method: { type: 'CARD', token: 'test-token' },
        amount: { currency: 'USD', value: 50.00 },
        country: 'CO',
        customer: { email: 'test@example.com' },
      }),
    });

    expect(response.status).toBe(200);
    const payment = await response.json();
    expect(payment.status).toBe('SUCCEEDED');
  });

  it('returns 400 for missing required fields', async () => {
    const response = await fetch(`${API_URL}/v1/payments`, {
      method: 'POST',
      headers: {
        'public-api-key': process.env.YUNO_PUBLIC_KEY,
        'private-secret-key': process.env.YUNO_PRIVATE_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        checkout_session: checkoutSession,
        // Missing payment_method, amount, country
      }),
    });

    expect(response.status).toBe(400);
  });
});

CI/CD Pipeline Integration

GitHub Actions

# .github/workflows/payment-tests.yml
name: Payment Integration Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run test:unit

  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run test:integration
        env:
          YUNO_PUBLIC_KEY: ${{ secrets.YUNO_SANDBOX_PUBLIC_KEY }}
          YUNO_PRIVATE_KEY: ${{ secrets.YUNO_SANDBOX_PRIVATE_KEY }}
          YUNO_ACCOUNT_CODE: ${{ secrets.YUNO_SANDBOX_ACCOUNT_CODE }}

  e2e-tests:
    runs-on: ubuntu-latest
    needs: integration-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
        env:
          YUNO_PUBLIC_KEY: ${{ secrets.YUNO_SANDBOX_PUBLIC_KEY }}
          YUNO_PRIVATE_KEY: ${{ secrets.YUNO_SANDBOX_PRIVATE_KEY }}
          YUNO_ACCOUNT_CODE: ${{ secrets.YUNO_SANDBOX_ACCOUNT_CODE }}

Environment secrets

Store your Yuno sandbox credentials as CI/CD secrets:
Secret NameValueNotes
YUNO_SANDBOX_PUBLIC_KEYYour sandbox public API keySafe for client and server
YUNO_SANDBOX_PRIVATE_KEYYour sandbox private secret keyServer-side only
YUNO_SANDBOX_ACCOUNT_CODEYour sandbox account codeRequired for all requests
YUNO_SANDBOX_WEBHOOK_SECRETYour sandbox webhook signing secretFor webhook tests
Never use production credentials in CI/CD pipelines. Always use sandbox credentials for automated testing.

Testing Different Payment Methods

Payment method test matrix

Structure your tests to cover each payment method you support:
// test/payment-methods.test.js
const paymentMethods = [
  {
    name: 'Card (Visa)',
    type: 'CARD',
    country: 'CO',
    currency: 'USD',
    extra: { token: 'test-token' },
  },
  {
    name: 'PIX',
    type: 'PIX',
    country: 'BR',
    currency: 'BRL',
    extra: {},
    customer: {
      document: { document_type: 'CPF', document_number: '12345678901' },
    },
  },
  {
    name: 'OXXO',
    type: 'OXXO',
    country: 'MX',
    currency: 'MXN',
    extra: {},
  },
];

describe.each(paymentMethods)('$name payment', (method) => {
  it(`creates a ${method.type} payment successfully`, async () => {
    const session = await createSession(method.country, method.currency);
    const payment = await createPayment({
      checkout_session: session,
      payment_method: { type: method.type, ...method.extra },
      amount: { currency: method.currency, value: 100.00 },
      country: method.country,
      customer: {
        email: 'test@example.com',
        ...method.customer,
      },
    });

    expect(['SUCCEEDED', 'PENDING']).toContain(payment.status);
  });
});

Sandbox vs. production parity

AspectSandboxProduction
API URLapi-sandbox.y.unoapi.y.uno
Test cardsAll test numbers workReal cards only
Payment completionSimulatedReal provider processing
WebhooksTest events availableReal events only
Rate limitsRelaxedStandard limits
FundsNo real moneyReal transactions
Always run your full test suite against sandbox before deploying to production. Use feature flags to switch between sandbox and production API URLs based on your environment.