MobileAppTesting mobile-app-testing

全面覆盖iOS和Android移动应用测试策略,包括单元测试、UI测试、集成测试、性能测试和测试自动化。

测试 0 次安装 0 次浏览 更新于 3/4/2026

移动应用测试

概览

实施全面的移动应用测试策略,包括单元测试、UI测试、集成测试和性能测试。

何时使用

  • 创建具有测试覆盖率的可靠移动应用
  • 在iOS和Android上自动化UI测试
  • 性能测试和优化
  • 与后端服务的集成测试
  • 发布前的回归测试

指令

1. React Native测试与Jest & Detox

// 单元测试与Jest
import { calculate } from '../utils/math';

describe('数学工具', () => {
  test('应该添加两个数字', () => {
    expect(calculate.add(2, 3)).toBe(5);
  });

  test('应该处理负数', () => {
    expect(calculate.add(-2, 3)).toBe(1);
  });
});

// 组件单元测试
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import { UserProfile } from '../components/UserProfile';

describe('UserProfile组件', () => {
  test('正确渲染用户名', () => {
    const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' };
    render(<UserProfile user={mockUser} />);

    expect(screen.getByText('John Doe')).toBeTruthy();
  });

  test('优雅地处理缺失的用户', () => {
    render(<UserProfile user={null} />);
    expect(screen.getByText(/没有用户数据/i)).toBeTruthy();
  });
});

// E2E测试与Detox
describe('登录流程E2E测试', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('应该用有效的凭证成功登录', async () => {
    await waitFor(element(by.id('emailInput')))
      .toBeVisible()
      .withTimeout(5000);

    await element(by.id('emailInput')).typeText('user@example.com');
    await element(by.id('passwordInput')).typeText('password123');
    await element(by.id('loginButton')).multiTap();

    await waitFor(element(by.text('Home Feed')))
      .toBeVisible()
      .withTimeout(5000);
  });

  it('应该显示错误信息与无效凭证', async () => {
    await element(by.id('emailInput')).typeText('invalid@example.com');
    await element(by.id('passwordInput')).typeText('wrongpass');
    await element(by.id('loginButton')).multiTap();

    await waitFor(element(by.text(/凭证无效/i)))
      .toBeVisible()
      .withTimeout(5000);
  });

  it('应该在标签间导航', async () => {
    await element(by.id('profileTab')).tap();
    await waitFor(element(by.text('Profile')))
      .toBeVisible()
      .withTimeout(2000);

    await element(by.id('homeTab')).tap();
    await waitFor(element(by.text('Home Feed')))
      .toBeVisible()
      .withTimeout(2000);
  });
});

2. iOS测试与XCTest

import XCTest
@testable import MyApp

class UserViewModelTests: XCTestCase {
  var viewModel: UserViewModel!
  var mockNetworkService: MockNetworkService!

  override func setUp() {
    super.setUp()
    mockNetworkService = MockNetworkService()
    viewModel = UserViewModel(networkService: mockNetworkService)
  }

  func testFetchUserSuccess() async {
    let expectedUser = User(id: UUID(), name: "John", email: "john@example.com")
    mockNetworkService.mockUser = expectedUser

    await viewModel.fetchUser(id: expectedUser.id)

    XCTAssertEqual(viewModel.user?.name, "John")
    XCTAssertNil(viewModel.errorMessage)
    XCTAssertFalse(viewModel.isLoading)
  }

  func testFetchUserFailure() async {
    mockNetworkService.shouldFail = true

    await viewModel.fetchUser(id: UUID())

    XCTAssertNil(viewModel.user)
    XCTAssertNotNil(viewModel.errorMessage)
    XCTAssertFalse(viewModel.isLoading)
  }
}

class MockNetworkService: NetworkService {
  var mockUser: User?
  var shouldFail = false

  override func fetch<T: Decodable>(
    _: T.Type,
    from endpoint: String
  ) async throws -> T {
    if shouldFail {
      throw NetworkError.unknown
    }
    return mockUser as! T
  }
}

// UI测试
class LoginUITests: XCTestCase {
  override func setUp() {
    super.setUp()
    continueAfterFailure = false
    XCUIApplication().launch()
  }

  func testLoginFlow() {
    let app = XCUIApplication()

    let emailTextField = app.textFields["emailInput"]
    let passwordTextField = app.secureTextFields["passwordInput"]
    let loginButton = app.buttons["loginButton"]

    emailTextField.tap()
    emailTextField.typeText("user@example.com")

    passwordTextField.tap()
    passwordTextField.typeText("password123")

    loginButton.tap()

    let homeText = app.staticTexts["Home Feed"]
    XCTAssertTrue(homeText.waitForExistence(timeout: 5))
  }

  func testNavigationBetweenTabs() {
    let app = XCUIApplication()
    let profileTab = app.tabBars.buttons["Profile"]
    let homeTab = app.tabBars.buttons["Home"]

    profileTab.tap()
    XCTAssertTrue(app.staticTexts["Profile"].exists)

    homeTab.tap()
    XCTAssertTrue(app.staticTexts["Home"].exists)
  }
}

3. Android测试与Espresso

@RunWith(AndroidJUnit4::class)
class UserViewModelTest {
  private lateinit var viewModel: UserViewModel
  private val mockApiService = mock<ApiService>()

  @Before
  fun setUp() {
    viewModel = UserViewModel(mockApiService)
  }

  @Test
  fun fetchUserSuccess() = runTest {
    val expectedUser = User("1", "John", "john@example.com")
    `when`(mockApiService.getUser("1")).thenReturn(expectedUser)

    viewModel.fetchUser("1")

    assertEquals(expectedUser.name, viewModel.user.value?.name)
    assertEquals(null, viewModel.errorMessage.value)
  }

  @Test
  fun fetchUserFailure() = runTest {
    `when`(mockApiService.getUser("1"))
      .thenThrow(IOException("Network error"))

    viewModel.fetchUser("1")

    assertEquals(null, viewModel.user.value)
    assertNotNull(viewModel.errorMessage.value)
  }
}

// UI测试与Espresso
@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
  @get:Rule
  val activityRule = ActivityScenarioRule(LoginActivity::class.java)

  @Test
  fun testLoginWithValidCredentials() {
    onView(withId(R.id.emailInput))
      .perform(typeText("user@example.com"))

    onView(withId(R.id.passwordInput))
      .perform(typeText("password123"))

    onView(withId(R.id.loginButton))
      .perform(click())

    onView(withText("Home"))
      .check(matches(isDisplayed()))
  }

  @Test
  fun testLoginWithInvalidCredentials() {
    onView(withId(R.id.emailInput))
      .perform(typeText("invalid@example.com"))

    onView(withId(R.id.passwordInput))
      .perform(typeText("wrongpassword"))

    onView(withId(R.id.loginButton))
      .perform(click())

    onView(withText(containsString("Invalid credentials")))
      .check(matches(isDisplayed()))
  }

  @Test
  fun testNavigationBetweenTabs() {
    onView(withId(R.id.profileTab)).perform(click())
    onView(withText("Profile")).check(matches(isDisplayed()))

    onView(withId(R.id.homeTab)).perform(click())
    onView(withText("Home")).check(matches(isDisplayed()))
  }
}

4. 性能测试

import XCTest

class PerformanceTests: XCTestCase {
  func testListRenderingPerformance() {
    let viewModel = ItemsViewModel()
    viewModel.items = (0..<1000).map { i in
      Item(id: UUID(), title: "Item \(i)", price: Double(i))
    }

    measure {
      _ = viewModel.items.filter { $0.price > 50 }
    }
  }

  func testNetworkResponseTime() {
    let networkService = NetworkService()

    measure {
      let expectation = XCTestExpectation(description: "Fetch user")

      Task {
        do {
          _ = try await networkService.fetch(User.self, from: "/users/test")
          expectation.fulfill()
        } catch {
          XCTFail("Network request failed")
        }
      }

      wait(for: [expectation], timeout: 10)
    }
  }
}

最佳实践

✅ 做

  • 首先编写业务逻辑测试
  • 使用依赖注入提高可测试性
  • 模拟外部API调用
  • 测试成功和失败路径
  • 自动化关键流程的UI测试
  • 在真实设备上运行测试
  • 在目标设备上测量性能
  • 保持测试独立和隔离
  • 使用有意义的测试名称
  • 保持>80%的代码覆盖率

❌ 不做

  • 跳过测试UI关键流程
  • 使用硬编码的测试数据
  • 忽略性能回归
  • 测试实现细节
  • 使测试变得不稳定或不可靠
  • 跳过在实际设备上的测试
  • 忽略可访问性测试
  • 创建相互依赖的测试
  • 测试时不模拟API
  • 部署未经测试的代码