Test your Drop-in integration in sandbox before going live.
Test your Drop-in integration in the sandbox environment before accepting real payments. The sandbox environment allows you to test all payment methods without processing actual transactions.
Set the environment to Test when initialising Drop-in:
CheckoutDropInConfig(
environment = Environment.TEST, // Use sandbox
// ... rest of config
)Key points about sandbox testing:
- No real money is charged.
- Use test card numbers and sandbox accounts.
- All payment methods work the same as production.
- Backend verification still required using test API credentials.
Use these test cards in sandbox to simulate different scenarios:
The following test cards represent successful payment scenarios:
| Card number | Network | 3DS behaviour |
|---|---|---|
4111 1111 1111 1111 | Visa | No challenge |
5555 5555 5555 4444 | Mastercard | No challenge |
3782 822463 10005 | American Express | No challenge |
4000 0000 0000 0002 | Visa | Challenge required |
For all test cards:
- Expiry date: Any future date (e.g.,
12/25). - CVV: Any 3 digits (e.g.,
123). - Cardholder name: Any name.
The following test cards simulate payment failures:
| Card number | Network | Result |
|---|---|---|
4000 0000 0000 0077 | Visa | Insufficient funds |
4000 0000 0000 0051 | Visa | Card declined |
4000 0000 0000 0069 | Visa | Expired card |
4000 0000 0000 0101 | Visa | CVV failure |
When using the challenge test card (4000 0000 0000 0002), a 3D Secure authentication screen appears in the native Android UI:
- To pass: enter any code (e.g.,
1234). - To fail: leave empty or enter wrong code multiple times.
- To timeout: wait for authentication to timeout.
Verify that your onSuccess callback fires after successful authentication, and that your backend receives the transaction details.
You need a PayPal sandbox account for testing. Create one in the PayPal Developer Dashboard.
Create a personal (buyer) account for testing customer payments.
- Tap the PayPal button in the drop-in.
- Log in with your PayPal sandbox account in the web view.
- Approve the payment.
- Verify that
onSuccessfires with transaction details. - Verify your backend receives the payment notification.
Test both transaction intents:
- Purchase intent: Funds captured immediately.
- Authorisation intent: Funds authorised but not captured (capture later via backend API).
- Android 7.0 (API level 24) or higher
- Google Play Services installed
- Google Account signed in
- Test card added to Google Wallet
- Open your app on an Android device or emulator.
- Sign in to your Google Account.
- Add a test card to Google Wallet (use test card numbers from above).
- Tap the Google Pay button in Drop-in.
- Select your test card in the payment sheet.
- Tap Pay.
- Verify that
onSuccessfires with transaction details.
The Google Pay button only appears when the user has cards in their Google Wallet and Google Play Services is available.
To test Google Pay on an Android emulator:
- Use an emulator with Google Play (API level 24+).
- Sign in with a Google Account.
- Open Google Wallet and add test cards manually.
- Run your app and test Google Pay integration.
Test that your error handling works correctly by simulating failures:
Use the failed payment test cards to trigger different error types:
- Insufficient funds:
4000 0000 0000 0077 - Card declined:
4000 0000 0000 0051 - Expired card:
4000 0000 0000 0069 - CVV failure:
4000 0000 0000 0101
Verify that your onError callback receives the error and displays an appropriate message to the customer.
Test that your error handling works correctly for network failures:
- Enable airplane mode on your device.
- Try to submit a payment.
- Verify that your
onErrorcallback receives the error. - Verify your error handling shows an appropriate network error message.
Verify that your error callback handles network failures correctly:
class NetworkErrorTest {
@Test
fun testNetworkErrorHandling() {
var errorReceived: BaseSdkException? = null
val config = CheckoutDropInConfig(
environment = Environment.TEST,
session = mockSessionData(),
ownerType = "MerchantGroup",
ownerId = "MERCHANT-1",
transactionData = mockTransactionData(),
onError = { error ->
errorReceived = error
}
)
// Simulate network error (SDK error code SDK0500)
config.onError?.invoke(mockNetworkError())
// Verify error was received and has correct code
assertNotNull(errorReceived)
assertEquals("SDK0500", errorReceived?.errorCode)
assertTrue(errorReceived?.message?.contains("network", ignoreCase = true) == true)
}
}Test cancellation handling:
- Card (3DS): close the 3D Secure authentication window without completing authentication.
- PayPal: close the web view without completing payment.
- Google Pay: tap outside the payment sheet or press Back.
Verify that your onCancel callback fires (if configured in methodConfig.global).
Always verify payments on your backend before fulfilling orders. The frontend callbacks can be manipulated by users.
Test these critical security checks:
- Transaction exists: query the PXP API with
systemTransactionIdto verify the transaction exists. - Amount matches: verify the transaction amount matches your expected amount.
- Transaction succeeded: check that the transaction state is
AuthorisedorCaptured. - Prevent replay attacks: ensure the same transaction can't be processed twice.
Example verification:
// Backend verification endpoint (Node.js/Express example)
app.post('/api/verify-payment', async (req, res) => {
const { systemTransactionId, merchantTransactionId } = req.body;
// Get expected amount from your database
const order = await db.orders.findOne({ merchantTransactionId });
const expectedAmount = order.amount;
// Query PXP API to verify transaction
const transaction = await unityApi.getTransaction(systemTransactionId);
// Verify transaction is successful
if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
return res.json({ success: false, error: 'Transaction not authorised' });
}
// Verify amount matches
const txnAmount = transaction.amounts?.transactionValue || transaction.amount;
if (Math.abs(txnAmount - expectedAmount) > 0.01) {
return res.json({ success: false, error: 'Amount mismatch' });
}
// Check for duplicate processing
const alreadyProcessed = await db.transactions.findOne({ systemTransactionId });
if (alreadyProcessed) {
return res.json({ success: true, orderId: alreadyProcessed.orderId });
}
// Process order
const orderId = await fulfillOrder(order);
await db.transactions.create({ systemTransactionId, orderId });
res.json({ success: true, orderId });
});Test on real Android devices to ensure the best user experience.
Test on devices that represent your user base:
- Budget device: Samsung Galaxy A series, Motorola Moto G
- Mid-range device: Google Pixel 6a, Samsung Galaxy S22
- Premium device: Google Pixel 8, Samsung Galaxy S24
- Tablet: Samsung Galaxy Tab S9
Test the following on each device:
- Card field rendering and keyboard input
- 3D Secure authentication UI
- Google Pay integration (if available)
- PayPal web view interaction
- Screen rotation handling
- Different screen sizes and densities
- Performance and responsiveness
Test your UI layout on different screen sizes using Compose previews:
// Preview different screen sizes for layout inspection
@Preview(name = "Phone", device = Devices.PHONE)
@Preview(name = "Foldable", device = Devices.FOLDABLE)
@Preview(name = "Tablet", device = Devices.TABLET)
@Composable
fun CheckoutScreenPreview() {
CheckoutScreen()
}Compose @Preview is useful for visual layout inspection during development, but it does not test runtime payment behaviour. Always run actual functional tests on emulators or physical devices to validate payment flows.
Write automated tests for your Drop-in integration:
Test your configuration and callback logic:
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import com.pxp.checkout.models.DropInTransactionData
import com.pxp.checkout.models.DropInTransactionIntentData
import com.pxp.checkout.models.Environment
import com.pxp.checkout.models.EntryType
import com.pxp.checkout.models.IntentType
import org.junit.Test
import org.junit.Assert.*
class CheckoutConfigTest {
@Test
fun testConfigCreation() {
val config = CheckoutDropInConfig(
environment = Environment.TEST,
session = mockSessionData(),
ownerType = "MerchantGroup",
ownerId = "MERCHANT-1",
transactionData = DropInTransactionData(
currency = "GBP",
amount = 99.99,
entryType = EntryType.Ecom,
intent = DropInTransactionIntentData(
card = IntentType.Authorisation
),
merchant = "Demo Store",
merchantTransactionId = "test-txn-123",
merchantTransactionDate = { "2026-05-18T12:00:00Z" }
)
)
assertEquals(Environment.TEST, config.environment)
assertEquals("MERCHANT-1", config.ownerId)
assertEquals("GBP", config.transactionData.currency)
}
@Test
fun testCallbackExecution() {
var successCalled = false
var errorCalled = false
val config = CheckoutDropInConfig(
environment = Environment.TEST,
session = mockSessionData(),
ownerType = "MerchantGroup",
ownerId = "MERCHANT-1",
transactionData = DropInTransactionData(
currency = "GBP",
amount = 99.99,
entryType = EntryType.Ecom,
intent = DropInTransactionIntentData(
card = IntentType.Authorisation
),
merchant = "Demo Store",
merchantTransactionId = "test-txn-123",
merchantTransactionDate = { "2026-05-18T12:00:00Z" }
),
onSuccess = { result ->
successCalled = true
},
onError = { error ->
errorCalled = true
}
)
// Test callbacks
config.onSuccess?.invoke(mockSuccessResult())
assertTrue(successCalled)
assertFalse(errorCalled)
}
}Test the full payment flow:
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test
class CheckoutFlowTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testCardPaymentFlow() {
composeTestRule.setContent {
CheckoutScreen()
}
// Wait for Drop-in to render
composeTestRule.waitForIdle()
// Verify payment methods are displayed
composeTestRule.onNodeWithText("Card").assertIsDisplayed()
composeTestRule.onNodeWithText("PayPal").assertIsDisplayed()
// Select card payment
composeTestRule.onNodeWithText("Card").performClick()
// Verify card form is displayed
composeTestRule.onNodeWithText("Card number").assertIsDisplayed()
composeTestRule.onNodeWithText("Expiry date").assertIsDisplayed()
composeTestRule.onNodeWithText("CVV").assertIsDisplayed()
}
@Test
fun testPaymentSubmission() {
composeTestRule.setContent {
CheckoutScreen()
}
// Wait for Drop-in to render
composeTestRule.waitForIdle()
// Fill card details and submit
composeTestRule.onNodeWithText("Card").performClick()
composeTestRule.onNodeWithText("Card number").performTextInput("4111111111111111")
composeTestRule.onNodeWithText("Expiry date").performTextInput("12/25")
composeTestRule.onNodeWithText("CVV").performTextInput("123")
composeTestRule.onNodeWithText("Pay").performClick()
// Verify payment submission
composeTestRule.waitForIdle()
}
}Before going live, verify the following:
- Change environment to
Environment.LIVE. - Use production API credentials.
- Verify correct
ownerIdandownerType. - Confirm currency and amount formatting.
- Test all enabled payment methods.
- Test successful payments.
- Test failed payments and error handling.
- Test user cancellation scenarios.
- Verify backend verification works correctly.
- Test on physical devices (not just emulators).
- Backend verification implemented.
- Replay attack prevention in place.
- HTTPS enabled on all backend endpoints.
- No sensitive data logged.
- ProGuard/R8 obfuscation enabled for production builds.
- Test on different screen sizes and orientations.
- Loading states display correctly.
- Success screen implemented.
- Error messages are user-friendly.
- Accessibility features tested (TalkBack, large text).
- App startup time is acceptable.
- Payment submission is responsive.
- No memory leaks detected.
- Network timeouts configured appropriately.
- Lint checks pass.
- Unit tests pass.
- Integration tests pass.
- Code review completed.
- Documentation updated.