KMP vs React Native vs Flutter: Testing Comparison
Choosing a cross-platform mobile framework is partly a testing decision. The tools, ecosystems, and philosophies around testing differ significantly between Kotlin Multiplatform, React Native, and Flutter. This comparison helps you understand the tradeoffs before you commit.
The Core Testing Philosophy Difference
KMP shares business logic but keeps native UI. Testing is split: shared logic tests in commonTest (runs on all platforms), UI tests use native tooling (Espresso/XCUITest) or Compose testing.
React Native renders a JavaScript app in a native bridge. Testing uses Jest for unit tests and Detox or WDIO for end-to-end tests that drive the actual app.
Flutter renders everything on its own canvas using Dart. It has the most unified testing story: flutter test runs widget, integration, and unit tests with consistent tooling across platforms.
Unit Testing
KMP
Tests in commonTest use kotlin.test:
class PriceCalculatorTest {
@Test
fun `applies discount correctly`() {
val result = PriceCalculator.withDiscount(base = 10000L, discountPercent = 20)
assertEquals(8000L, result)
}
}Run via: ./gradlew :shared:jvmTest
Strengths:
- Same tests run on JVM, Android, and iOS
- Fast JVM execution for development
- Full Kotlin language — coroutines, sealed classes, data classes
Weaknesses:
- UI can't be unit-tested in
commonTest - Platform-specific behavior requires separate test sets
React Native
Jest is the standard unit test runner. Uses @testing-library/react-native for component tests:
// __tests__/priceCalculator.test.js
import { withDiscount } from '../src/priceCalculator';
test('applies discount correctly', () => {
expect(withDiscount(10000, 20)).toBe(8000);
});// Component test
import { render, fireEvent } from '@testing-library/react-native';
import { CartScreen } from '../src/CartScreen';
test('shows total after adding item', () => {
const { getByText, getByTestId } = render(<CartScreen />);
fireEvent.press(getByTestId('add-widget'));
expect(getByText('Total: $10.00')).toBeTruthy();
});Strengths:
- Jest is fast and familiar to JS developers
- React Testing Library approach works on components (no device needed)
- Huge ecosystem of testing utilities
Weaknesses:
- Component tests run in a JS virtual DOM — behavior can differ from real device
- No built-in way to test the JS-to-native bridge
- Platform differences (iOS vs Android bridge behavior) often only caught in E2E
Flutter
Dart tests use flutter_test:
// test/price_calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/price_calculator.dart';
void main() {
test('applies discount correctly', () {
final result = PriceCalculator.withDiscount(base: 10000, discountPercent: 20);
expect(result, 8000);
});
}Widget tests:
testWidgets('shows total after adding item', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.byKey(Key('add-widget')));
await tester.pump();
expect(find.text('Total: \$10.00'), findsOneWidget);
});Strengths:
- Widget tests are fast (no simulator, no bridge)
- Same Dart/Flutter code runs on all platforms — tests are truly shared
- Built-in golden image testing for visual regression
Weaknesses:
- Flutter's custom renderer means widget tests don't exercise native UI components
- Dart is a smaller ecosystem than Kotlin or JavaScript
UI Testing
KMP
Compose Multiplatform uses ComposeTestRule (Android) and runComposeUiTest (desktop). For native Android/iOS UI (non-Compose), use the native tools:
| Platform | Tool | Runs Where |
|---|---|---|
| Android (Compose) | ComposeTestRule | Emulator/device |
| Android (XML) | Espresso | Emulator/device |
| iOS | XCUITest | Simulator/device |
| Desktop (Compose) | runComposeUiTest | JVM, no device |
There is no unified "write once, test UI everywhere" solution in KMP. You write UI tests per platform.
React Native
Detox is the standard E2E testing framework for React Native:
// e2e/cart.test.js
describe('Cart', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should show total after adding item', async () => {
await element(by.id('add-widget')).tap();
await expect(element(by.text('Total: $10.00'))).toBeVisible();
});
});Detox drives real iOS/Android apps via native accessibility APIs. It requires a simulator or device, but runs on CI reliably once configured.
Alternative: WebdriverIO with Appium — more verbose but more flexible.
Flutter
Integration tests use flutter_driver or the newer integration_test package:
// integration_test/cart_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('shows total after adding item', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.byKey(Key('add-widget')));
await tester.pumpAndSettle();
expect(find.text('Total: \$10.00'), findsOneWidget);
});
}Strengths:
- Integration tests use the same
flutter_testAPI as widget tests - Golden image testing is built-in
- Runs on iOS, Android, and desktop with the same code
Weaknesses:
- Flutter renders on its own canvas — tests don't exercise native accessibility tree
- Screen reader testing requires extra configuration
End-to-End Testing
All three frameworks ultimately need real-device E2E testing for production confidence.
| Framework | Primary E2E Tool | Notes |
|---|---|---|
| KMP | Espresso + XCUITest (separate) | Per-platform setup |
| React Native | Detox | Cross-platform, JS API |
| Flutter | integration_test | Cross-platform, Dart API |
For all three, HelpMeTest provides continuous E2E testing that monitors your live app 24/7. Write test scenarios in plain English — no framework-specific DSL to learn. HelpMeTest catches production regressions that local tests miss.
CI Configuration Complexity
KMP
jobs:
jvm-tests: # Ubuntu, fast
android-tests: # Ubuntu, needs emulator (slow)
ios-tests: # macOS required (expensive)Three separate jobs, three different runner environments.
React Native
jobs:
unit-tests: # Ubuntu, Jest only, fast
ios-e2e: # macOS, Detox + simulator
android-e2e: # Ubuntu, Detox + emulatorSimilar complexity. The Jest unit tests are fast; Detox jobs are slow.
Flutter
jobs:
unit-widget-tests: # Ubuntu, flutter test, fast
ios-integration: # macOS, integration_test
android-integration: # Ubuntu, integration_testThe unit/widget tests are unified. Integration tests still split by platform.
Developer Experience Comparison
| Aspect | KMP | React Native | Flutter |
|---|---|---|---|
| Unit test speed | Fast (JVM) | Fast (Node.js) | Fast (Dart VM) |
| UI test unification | None | Partial (Detox) | Good (integration_test) |
| Type safety | Excellent | Moderate (TS) | Good (Dart) |
| Test isolation | Good | Good | Excellent (widget sandbox) |
| E2E tooling | Per-platform | Detox | integration_test |
| iOS CI cost | High (macOS) | High (macOS) | High (macOS) |
| Team familiarity | Kotlin devs | JS/TS devs | Dart devs |
Which Is Best for Testing?
Choose KMP if:
- Your team knows Kotlin well
- You want native UI per platform with shared business logic
- You're adding cross-platform to an existing Android app
- You need Compose on Android and want to share logic with iOS
Choose React Native if:
- Your team is JavaScript-first
- You want Detox's relatively unified E2E story
- You're building a content app where UI fidelity is less critical
Choose Flutter if:
- You want the most unified testing story (single test codebase for unit + widget + integration)
- Custom UI rendering is acceptable
- Your team can adopt Dart
The honest answer: No framework makes mobile testing easy. All three require platform-specific CI infrastructure for iOS. All three have gaps between what unit tests catch and what real-device tests catch.
What matters more than the framework choice is testing discipline: running tests on real targets, maintaining CI pipelines, and monitoring production behavior continuously.
Summary
| Testing Layer | KMP Winner | Notes |
|---|---|---|
| Unit tests | Tie (KMP/Flutter) | Both strongly typed, fast |
| UI tests | Flutter | Most unified story |
| E2E tests | React Native | Detox is mature |
| CI simplicity | Flutter | integration_test is cross-platform |
| Type safety | KMP | Kotlin is excellent |
For production applications, the unit/integration test story is table stakes. The real differentiator is whether you have continuous end-to-end monitoring catching regressions before users report them — and that's independent of your framework choice.