Mobile App Testing
Overview
Implement comprehensive testing strategies for mobile applications including unit tests, UI tests, integration tests, and performance testing.
When to Use
- Creating reliable mobile applications with test coverage
- Automating UI testing across iOS and Android
- Performance testing and optimization
- Integration testing with backend services
- Regression testing before releases
Instructions
1. React Native Testing with Jest & Detox
javascript
1// Unit test with Jest
2import { calculate } from '../utils/math';
3
4describe('Math utilities', () => {
5 test('should add two numbers', () => {
6 expect(calculate.add(2, 3)).toBe(5);
7 });
8
9 test('should handle negative numbers', () => {
10 expect(calculate.add(-2, 3)).toBe(1);
11 });
12});
13
14// Component unit test
15import React from 'react';
16import { render, screen } from '@testing-library/react-native';
17import { UserProfile } from '../components/UserProfile';
18
19describe('UserProfile Component', () => {
20 test('renders user name correctly', () => {
21 const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' };
22 render(<UserProfile user={mockUser} />);
23
24 expect(screen.getByText('John Doe')).toBeTruthy();
25 });
26
27 test('handles missing user gracefully', () => {
28 render(<UserProfile user={null} />);
29 expect(screen.getByText(/no user data/i)).toBeTruthy();
30 });
31});
32
33// E2E Testing with Detox
34describe('Login Flow E2E Test', () => {
35 beforeAll(async () => {
36 await device.launchApp();
37 });
38
39 beforeEach(async () => {
40 await device.reloadReactNative();
41 });
42
43 it('should login successfully with valid credentials', async () => {
44 await waitFor(element(by.id('emailInput')))
45 .toBeVisible()
46 .withTimeout(5000);
47
48 await element(by.id('emailInput')).typeText('user@example.com');
49 await element(by.id('passwordInput')).typeText('password123');
50 await element(by.id('loginButton')).multiTap();
51
52 await waitFor(element(by.text('Home Feed')))
53 .toBeVisible()
54 .withTimeout(5000);
55 });
56
57 it('should show error with invalid credentials', async () => {
58 await element(by.id('emailInput')).typeText('invalid@example.com');
59 await element(by.id('passwordInput')).typeText('wrongpass');
60 await element(by.id('loginButton')).multiTap();
61
62 await waitFor(element(by.text(/invalid credentials/i)))
63 .toBeVisible()
64 .withTimeout(5000);
65 });
66
67 it('should navigate between tabs', async () => {
68 await element(by.id('profileTab')).tap();
69 await waitFor(element(by.text('Profile')))
70 .toBeVisible()
71 .withTimeout(2000);
72
73 await element(by.id('homeTab')).tap();
74 await waitFor(element(by.text('Home Feed')))
75 .toBeVisible()
76 .withTimeout(2000);
77 });
78});
2. iOS Testing with XCTest
swift
1import XCTest
2@testable import MyApp
3
4class UserViewModelTests: XCTestCase {
5 var viewModel: UserViewModel!
6 var mockNetworkService: MockNetworkService!
7
8 override func setUp() {
9 super.setUp()
10 mockNetworkService = MockNetworkService()
11 viewModel = UserViewModel(networkService: mockNetworkService)
12 }
13
14 func testFetchUserSuccess() async {
15 let expectedUser = User(id: UUID(), name: "John", email: "john@example.com")
16 mockNetworkService.mockUser = expectedUser
17
18 await viewModel.fetchUser(id: expectedUser.id)
19
20 XCTAssertEqual(viewModel.user?.name, "John")
21 XCTAssertNil(viewModel.errorMessage)
22 XCTAssertFalse(viewModel.isLoading)
23 }
24
25 func testFetchUserFailure() async {
26 mockNetworkService.shouldFail = true
27
28 await viewModel.fetchUser(id: UUID())
29
30 XCTAssertNil(viewModel.user)
31 XCTAssertNotNil(viewModel.errorMessage)
32 XCTAssertFalse(viewModel.isLoading)
33 }
34}
35
36class MockNetworkService: NetworkService {
37 var mockUser: User?
38 var shouldFail = false
39
40 override func fetch<T: Decodable>(
41 _: T.Type,
42 from endpoint: String
43 ) async throws -> T {
44 if shouldFail {
45 throw NetworkError.unknown
46 }
47 return mockUser as! T
48 }
49}
50
51// UI Test
52class LoginUITests: XCTestCase {
53 override func setUp() {
54 super.setUp()
55 continueAfterFailure = false
56 XCUIApplication().launch()
57 }
58
59 func testLoginFlow() {
60 let app = XCUIApplication()
61
62 let emailTextField = app.textFields["emailInput"]
63 let passwordTextField = app.secureTextFields["passwordInput"]
64 let loginButton = app.buttons["loginButton"]
65
66 emailTextField.tap()
67 emailTextField.typeText("user@example.com")
68
69 passwordTextField.tap()
70 passwordTextField.typeText("password123")
71
72 loginButton.tap()
73
74 let homeText = app.staticTexts["Home Feed"]
75 XCTAssertTrue(homeText.waitForExistence(timeout: 5))
76 }
77
78 func testNavigationBetweenTabs() {
79 let app = XCUIApplication()
80 let profileTab = app.tabBars.buttons["Profile"]
81 let homeTab = app.tabBars.buttons["Home"]
82
83 profileTab.tap()
84 XCTAssertTrue(app.staticTexts["Profile"].exists)
85
86 homeTab.tap()
87 XCTAssertTrue(app.staticTexts["Home"].exists)
88 }
89}
3. Android Testing with Espresso
kotlin
1@RunWith(AndroidJUnit4::class)
2class UserViewModelTest {
3 private lateinit var viewModel: UserViewModel
4 private val mockApiService = mock<ApiService>()
5
6 @Before
7 fun setUp() {
8 viewModel = UserViewModel(mockApiService)
9 }
10
11 @Test
12 fun fetchUserSuccess() = runTest {
13 val expectedUser = User("1", "John", "john@example.com")
14 `when`(mockApiService.getUser("1")).thenReturn(expectedUser)
15
16 viewModel.fetchUser("1")
17
18 assertEquals(expectedUser.name, viewModel.user.value?.name)
19 assertEquals(null, viewModel.errorMessage.value)
20 }
21
22 @Test
23 fun fetchUserFailure() = runTest {
24 `when`(mockApiService.getUser("1"))
25 .thenThrow(IOException("Network error"))
26
27 viewModel.fetchUser("1")
28
29 assertEquals(null, viewModel.user.value)
30 assertNotNull(viewModel.errorMessage.value)
31 }
32}
33
34// UI Test with Espresso
35@RunWith(AndroidJUnit4::class)
36class LoginActivityTest {
37 @get:Rule
38 val activityRule = ActivityScenarioRule(LoginActivity::class.java)
39
40 @Test
41 fun testLoginWithValidCredentials() {
42 onView(withId(R.id.emailInput))
43 .perform(typeText("user@example.com"))
44
45 onView(withId(R.id.passwordInput))
46 .perform(typeText("password123"))
47
48 onView(withId(R.id.loginButton))
49 .perform(click())
50
51 onView(withText("Home"))
52 .check(matches(isDisplayed()))
53 }
54
55 @Test
56 fun testLoginWithInvalidCredentials() {
57 onView(withId(R.id.emailInput))
58 .perform(typeText("invalid@example.com"))
59
60 onView(withId(R.id.passwordInput))
61 .perform(typeText("wrongpassword"))
62
63 onView(withId(R.id.loginButton))
64 .perform(click())
65
66 onView(withText(containsString("Invalid credentials")))
67 .check(matches(isDisplayed()))
68 }
69
70 @Test
71 fun testNavigationBetweenTabs() {
72 onView(withId(R.id.profileTab)).perform(click())
73 onView(withText("Profile")).check(matches(isDisplayed()))
74
75 onView(withId(R.id.homeTab)).perform(click())
76 onView(withText("Home")).check(matches(isDisplayed()))
77 }
78}
swift
1import XCTest
2
3class PerformanceTests: XCTestCase {
4 func testListRenderingPerformance() {
5 let viewModel = ItemsViewModel()
6 viewModel.items = (0..<1000).map { i in
7 Item(id: UUID(), title: "Item \(i)", price: Double(i))
8 }
9
10 measure {
11 _ = viewModel.items.filter { $0.price > 50 }
12 }
13 }
14
15 func testNetworkResponseTime() {
16 let networkService = NetworkService()
17
18 measure {
19 let expectation = XCTestExpectation(description: "Fetch user")
20
21 Task {
22 do {
23 _ = try await networkService.fetch(User.self, from: "/users/test")
24 expectation.fulfill()
25 } catch {
26 XCTFail("Network request failed")
27 }
28 }
29
30 wait(for: [expectation], timeout: 10)
31 }
32 }
33}
Best Practices
✅ DO
- Write tests for business logic first
- Use dependency injection for testability
- Mock external API calls
- Test both success and failure paths
- Automate UI testing for critical flows
- Run tests on real devices
- Measure performance on target devices
- Keep tests isolated and independent
- Use meaningful test names
- Maintain >80% code coverage
❌ DON'T
- Skip testing UI-critical flows
- Use hardcoded test data
- Ignore performance regressions
- Test implementation details
- Make tests flaky or unreliable
- Skip testing on actual devices
- Ignore accessibility testing
- Create interdependent tests
- Test without mocking APIs
- Deploy untested code