NativeScript 测试策略 #
测试概述 #
测试是保证应用质量的重要手段,NativeScript 支持多种测试方式。
text
┌─────────────────────────────────────────────────────────────┐
│ 测试类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 单元测试 │
│ ├── 测试独立函数和类 │
│ ├── Jest / Mocha │
│ └── 快速执行 │
│ │
│ 组件测试 │
│ ├── 测试 UI 组件 │
│ └── 测试组件交互 │
│ │
│ E2E 测试 │
│ ├── 端到端测试 │
│ ├── Appium / Detox │
│ └── 真实设备/模拟器 │
│ │
└─────────────────────────────────────────────────────────────┘
单元测试 #
配置 Jest #
bash
npm install jest @types/jest ts-jest --save-dev
javascript
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.spec.ts'],
moduleFileExtensions: ['ts', 'js'],
transform: {
'^.+\\.ts$': 'ts-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/app/$1'
}
};
测试服务 #
typescript
// services/user.service.ts
export class UserService {
constructor(private api: ApiService) {}
async getUser(id: number): Promise<User> {
return this.api.get<User>(`/users/${id}`);
}
async createUser(data: CreateUserDto): Promise<User> {
return this.api.post<User>('/users', data);
}
}
// services/user.service.spec.ts
describe('UserService', () => {
let service: UserService;
let apiMock: jest.Mocked<ApiService>;
beforeEach(() => {
apiMock = {
get: jest.fn(),
post: jest.fn()
} as any;
service = new UserService(apiMock);
});
describe('getUser', () => {
it('should return user by id', async () => {
const mockUser = { id: 1, name: 'John' };
apiMock.get.mockResolvedValue(mockUser);
const result = await service.getUser(1);
expect(result).toEqual(mockUser);
expect(apiMock.get).toHaveBeenCalledWith('/users/1');
});
it('should handle error', async () => {
apiMock.get.mockRejectedValue(new Error('Not found'));
await expect(service.getUser(999)).rejects.toThrow('Not found');
});
});
describe('createUser', () => {
it('should create user', async () => {
const createData = { name: 'John', email: 'john@example.com' };
const mockUser = { id: 1, ...createData };
apiMock.post.mockResolvedValue(mockUser);
const result = await service.createUser(createData);
expect(result).toEqual(mockUser);
expect(apiMock.post).toHaveBeenCalledWith('/users', createData);
});
});
});
测试 ViewModel #
typescript
// viewmodels/home.viewmodel.ts
export class HomeViewModel extends Observable {
private _items: ObservableArray<Item>;
private _isLoading: boolean = false;
constructor(private itemService: ItemService) {
super();
this._items = new ObservableArray();
}
get items(): ObservableArray<Item> {
return this._items;
}
get isLoading(): boolean {
return this._isLoading;
}
async loadItems(): Promise<void> {
this._isLoading = true;
this.notifyPropertyChange('isLoading', true);
try {
const items = await this.itemService.getItems();
this._items.splice(0, this._items.length, ...items);
} finally {
this._isLoading = false;
this.notifyPropertyChange('isLoading', false);
}
}
}
// viewmodels/home.viewmodel.spec.ts
describe('HomeViewModel', () => {
let viewModel: HomeViewModel;
let itemServiceMock: jest.Mocked<ItemService>;
beforeEach(() => {
itemServiceMock = {
getItems: jest.fn()
} as any;
viewModel = new HomeViewModel(itemServiceMock);
});
describe('loadItems', () => {
it('should load items', async () => {
const mockItems = [
{ id: 1, title: 'Item 1' },
{ id: 2, title: 'Item 2' }
];
itemServiceMock.getItems.mockResolvedValue(mockItems);
await viewModel.loadItems();
expect(viewModel.items.length).toBe(2);
expect(viewModel.items.getItem(0)).toEqual(mockItems[0]);
});
it('should set loading state', async () => {
itemServiceMock.getItems.mockImplementation(() =>
new Promise(resolve => setTimeout(resolve, 100))
);
const promise = viewModel.loadItems();
expect(viewModel.isLoading).toBe(true);
await promise;
expect(viewModel.isLoading).toBe(false);
});
});
});
测试工具函数 #
typescript
// utils/formatters.ts
export function formatPrice(price: number, currency: string = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(price);
}
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString();
}
// utils/formatters.spec.ts
describe('Formatters', () => {
describe('formatPrice', () => {
it('should format price with default currency', () => {
expect(formatPrice(100)).toBe('$100.00');
});
it('should format price with custom currency', () => {
expect(formatPrice(100, 'EUR')).toBe('€100.00');
});
it('should handle decimal values', () => {
expect(formatPrice(99.99)).toBe('$99.99');
});
});
describe('formatDate', () => {
it('should format date', () => {
const date = new Date('2024-01-15');
const result = formatDate(date);
expect(result).toContain('2024');
});
it('should handle string date', () => {
const result = formatDate('2024-01-15');
expect(result).toContain('2024');
});
});
});
Angular 测试 #
配置测试 #
typescript
// src/app.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});
测试组件 #
typescript
// components/home/home.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
import { UserService } from '../../services/user.service';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
let userServiceMock: jasmine.SpyObj<UserService>;
beforeEach(async () => {
userServiceMock = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
declarations: [HomeComponent],
providers: [
{ provide: UserService, useValue: userServiceMock }
]
}).compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load users on init', () => {
const mockUsers = [{ id: 1, name: 'John' }];
userServiceMock.getUsers.and.returnValue(of(mockUsers));
component.ngOnInit();
expect(component.users).toEqual(mockUsers);
});
});
测试服务 #
typescript
// services/user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should get users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
E2E 测试 #
配置 Appium #
bash
npm install appium @nativescript/appium --save-dev
typescript
// e2e/setup.ts
import { AppiumDriver, createDriver } from 'nativescript-appium';
describe('E2E Tests', () => {
let driver: AppiumDriver;
before(async () => {
driver = await createDriver({
appPath: '/path/to/app.apk',
platformName: 'Android'
});
});
after(async () => {
await driver.quit();
});
it('should display login screen', async () => {
const title = await driver.findElementByText('Login');
expect(await title.text()).toBe('Login');
});
it('should login successfully', async () => {
const emailInput = await driver.findElementByXPath('//TextField[@hint="Email"]');
await emailInput.sendKeys('test@example.com');
const passwordInput = await driver.findElementByXPath('//TextField[@hint="Password"]');
await passwordInput.sendKeys('password123');
const loginButton = await driver.findElementByText('Login');
await loginButton.click();
const homeTitle = await driver.findElementByText('Home');
expect(await homeTitle.isDisplayed()).toBe(true);
});
});
使用 Detox #
bash
npm install detox --save-dev
javascript
// .detoxrc.js
module.exports = {
testRunner: {
args: {
'$0': 'jest',
config: 'e2e/jest.config.js'
},
jest: {
setupTimeout: 120000
}
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'platforms/ios/build/Debug-iphonesimulator/YourApp.app'
},
'android.debug': {
type: 'android.apk',
binaryPath: 'platforms/android/app/build/outputs/apk/debug/app-debug.apk'
}
},
devices: {
simulator: {
type: 'ios.simulator',
device: { type: 'iPhone 14' }
},
emulator: {
type: 'android.emulator',
device: { avdName: 'Pixel_5_API_33' }
}
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug'
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug'
}
}
};
typescript
// e2e/login.test.ts
describe('Login', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should show login screen', async () => {
await expect(element(by.text('Login'))).toBeVisible();
});
it('should login successfully', async () => {
await element(by.id('email-input')).typeText('test@example.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await expect(element(by.text('Home'))).toBeVisible();
});
});
测试最佳实践 #
测试原则 #
text
┌─────────────────────────────────────────────────────────────┐
│ 测试原则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. AAA 模式 │
│ Arrange - 准备测试数据 │
│ Act - 执行测试操作 │
│ Assert - 验证结果 │
│ │
│ 2. 单一职责 │
│ 每个测试只验证一个功能 │
│ │
│ 3. 独立性 │
│ 测试之间不应相互依赖 │
│ │
│ 4. 可重复 │
│ 测试结果应该可重复 │
│ │
│ 5. 命名清晰 │
│ 测试名称应描述测试内容 │
│ │
└─────────────────────────────────────────────────────────────┘
Mock 最佳实践 #
typescript
// 创建可复用的 Mock
export function createMockApiService(): jest.Mocked<ApiService> {
return {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn()
};
}
// 使用工厂函数
export function createMockUser(overrides?: Partial<User>): User {
return {
id: 1,
name: 'Test User',
email: 'test@example.com',
...overrides
};
}
测试覆盖率 #
javascript
// jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70
}
}
};
下一步 #
现在你已经掌握了测试策略,接下来学习 Angular集成,了解如何与 Angular 框架集成!
最后更新:2026-03-29